diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs index 7d612effb..e01494501 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs @@ -9,7 +9,7 @@ namespace Barotrauma { public override void DebugDraw(SpriteBatch spriteBatch) { - if (Character.IsDead) return; + if (Character.IsUnconscious || !Character.Enabled || !Enabled) { return; } Vector2 pos = Character.WorldPosition; pos.Y = -pos.Y; @@ -38,7 +38,7 @@ namespace Barotrauma } targetPos.Y = -targetPos.Y; GUI.DrawLine(spriteBatch, pos, targetPos, GUI.Style.Red * 0.5f, 0, 4); - if (wallTarget != null && (State == AIState.Attack || State == AIState.Aggressive || State == AIState.PassiveAggressive)) + if (wallTarget != null) { Vector2 wallTargetPos = wallTarget.Position; if (wallTarget.Structure.Submarine != null) { wallTargetPos += wallTarget.Structure.Submarine.Position; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index a84241519..8eca0c227 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -507,9 +507,10 @@ namespace Barotrauma { continue; } - if (item.body != null && !item.body.Enabled) continue; - if (item.ParentInventory != null) continue; - if (ignoredItems != null && ignoredItems.Contains(item)) continue; + if (item.body != null && !item.body.Enabled) { continue; } + if (item.ParentInventory != null) { continue; } + if (ignoredItems != null && ignoredItems.Contains(item)) { continue; } + if (item.Prefab.RequireCampaignInteract && item.CampaignInteractionType == CampaignMode.InteractionType.None) { continue; } if (Screen.Selected is SubEditorScreen editor && editor.WiringMode && item.GetComponent() == null) { continue; } if (draggingItemToWorld) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index 51f691267..0223840df 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -400,7 +400,7 @@ namespace Barotrauma if (Vector2.DistanceSquared(character.Position, item.Position) > 500f*500f) { continue; } var body = Submarine.CheckVisibility(character.SimPosition, item.SimPosition, ignoreLevel: true); if (body != null && body.UserData as Item != item) { continue; } - GUI.DrawIndicator(spriteBatch, item.WorldPosition + new Vector2(0f, item.RectHeight * 0.65f), cam, new Vector2(-100f, 500.0f), item.IconStyle.GetDefaultSprite(), item.IconStyle.Color); + GUI.DrawIndicator(spriteBatch, item.WorldPosition + new Vector2(0f, item.RectHeight * 0.65f), cam, new Vector2(-100f, 500.0f), item.IconStyle.GetDefaultSprite(), item.IconStyle.Color, createOffset: false); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index 8d590a191..718df5313 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -396,7 +396,9 @@ namespace Barotrauma break; case 6: //NetEntityEvent.Type.AssignCampaignInteraction byte campaignInteractionType = msg.ReadByte(); + bool requireConsciousness = msg.ReadBoolean(); (GameMain.GameSession?.GameMode as CampaignMode)?.AssignNPCMenuInteraction(this, (CampaignMode.InteractionType)campaignInteractionType); + RequireConsciousnessForCustomInteract = requireConsciousness; break; case 7: //NetEntityEvent.Type.ObjectiveManagerState // 1 = order, 2 = objective @@ -458,7 +460,7 @@ namespace Barotrauma { DebugConsole.Log("Reading character spawn data"); - if (GameMain.Client == null) return null; + if (GameMain.Client == null) { return null; } bool noInfo = inc.ReadBoolean(); ushort id = inc.ReadUInt16(); @@ -474,7 +476,15 @@ namespace Barotrauma Character character = null; if (noInfo) { - character = Create(speciesName, position, seed, characterInfo: null, id: id, isRemotePlayer: false); + try + { + character = Create(speciesName, position, seed, characterInfo: null, id: id, isRemotePlayer: false); + } + catch (Exception e) + { + DebugConsole.ThrowError($"Failed to spawn character {speciesName}", e); + throw; + } bool containsStatusData = inc.ReadBoolean(); if (containsStatusData) { @@ -490,8 +500,15 @@ namespace Barotrauma string infoSpeciesName = inc.ReadString(); CharacterInfo info = CharacterInfo.ClientRead(infoSpeciesName, inc); - - character = Create(speciesName, position, seed, characterInfo: info, id: id, isRemotePlayer: ownerId > 0 && GameMain.Client.ID != ownerId, hasAi: hasAi); + try + { + character = Create(speciesName, position, seed, characterInfo: info, id: id, isRemotePlayer: ownerId > 0 && GameMain.Client.ID != ownerId, hasAi: hasAi); + } + catch (Exception e) + { + DebugConsole.ThrowError($"Failed to spawn character {speciesName}", e); + throw; + } character.TeamID = (CharacterTeamType)teamID; character.CampaignInteractionType = (CampaignMode.InteractionType)inc.ReadByte(); if (character.CampaignInteractionType != CampaignMode.InteractionType.None) diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index fbe2dc370..dea65b297 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -241,6 +241,7 @@ namespace Barotrauma case "toggleupperhud": case "togglecharacternames": case "fpscounter": + case "showperf": case "dumptofile": case "findentityids": case "setfreecamspeed": diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/PirateMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/PirateMission.cs index 4391a2d05..fa71317bc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/PirateMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/PirateMission.cs @@ -16,7 +16,6 @@ namespace Barotrauma for (int j = 0; j < itemCount; j++) { Item.ReadSpawnData(msg); - } } if (characters.Contains(null)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index 1285c03b8..ecaca21ef 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -239,6 +239,9 @@ namespace Barotrauma private static SavingIndicatorState savingIndicatorState = SavingIndicatorState.None; private static float? timeUntilSavingIndicatorDisabled; + private static string loadedSpritesText; + private static DateTime loadedSpritesUpdateTime; + private enum SavingIndicatorState { None, @@ -454,9 +457,12 @@ namespace Barotrauma "Particle count: " + GameMain.ParticleManager.ParticleCount + "/" + GameMain.ParticleManager.MaxParticles, Color.Lerp(GUI.Style.Green, GUI.Style.Red, (GameMain.ParticleManager.ParticleCount / (float)GameMain.ParticleManager.MaxParticles)), Color.Black * 0.5f, 0, SmallFont); - DrawString(spriteBatch, new Vector2(10, 115), - "Loaded sprites: " + Sprite.LoadedSprites.Count() + "\n(" + Sprite.LoadedSprites.Select(s => s.FilePath).Distinct().Count() + " unique textures)", - Color.White, Color.Black * 0.5f, 0, SmallFont); + if (loadedSpritesText == null || DateTime.Now > loadedSpritesUpdateTime) + { + loadedSpritesText = "Loaded sprites: " + Sprite.LoadedSprites.Count() + "\n(" + Sprite.LoadedSprites.Select(s => s.FilePath).Distinct().Count() + " unique textures)"; + loadedSpritesUpdateTime = DateTime.Now + new TimeSpan(0, 0, seconds: 5); + } + DrawString(spriteBatch, new Vector2(10, 115), loadedSpritesText, Color.White, Color.Black * 0.5f, 0, SmallFont); if (debugDrawSounds) { @@ -1365,8 +1371,9 @@ namespace Barotrauma float screenDist = Vector2.Distance(cam.WorldToScreen(cam.WorldViewCenter), targetScreenPos); float angle = MathUtils.VectorToAngle(diff); + float originalAngle = angle; - float minAngleDiff = 0.05f; + const float minAngleDiff = 0.05f; bool overlapFound = true; int iterations = 0; while (overlapFound && iterations < 10) @@ -1388,18 +1395,24 @@ namespace Barotrauma usedIndicatorAngles.Add(angle); - Vector2 unclampedDiff = new Vector2( - (float)Math.Cos(angle) * screenDist, - (float)-Math.Sin(angle) * screenDist); - Vector2 iconDiff = new Vector2( + (float)Math.Cos(angle) * Math.Min(GameMain.GraphicsWidth * 0.4f, screenDist + 10), + (float)-Math.Sin(angle) * Math.Min(GameMain.GraphicsHeight * 0.4f, screenDist + 10)); + + angle = MathHelper.Lerp(originalAngle, angle, MathHelper.Clamp(((screenDist + 10f) - iconDiff.Length()) / 10f, 0f, 1f)); + + /*Vector2 unclampedDiff = new Vector2( + (float)Math.Cos(angle) * screenDist, + (float)-Math.Sin(angle) * screenDist);*/ + + iconDiff = new Vector2( (float)Math.Cos(angle) * Math.Min(GameMain.GraphicsWidth * 0.4f, screenDist), (float)-Math.Sin(angle) * Math.Min(GameMain.GraphicsHeight * 0.4f, screenDist)); Vector2 iconPos = cam.WorldToScreen(cam.WorldViewCenter) + iconDiff; sprite.Draw(spriteBatch, iconPos, color * alpha, rotate: 0.0f, scale: symbolScale); - if (unclampedDiff.Length() - 10 > iconDiff.Length()) + if (/*unclampedDiff.Length()*/ screenDist - 10 > iconDiff.Length()) { Vector2 normalizedDiff = Vector2.Normalize(targetScreenPos - iconPos); Vector2 arrowOffset = normalizedDiff * sprite.size.X * symbolScale * 0.7f; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs index 3272d4940..0d6f2b1e3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -838,6 +838,7 @@ namespace Barotrauma { bool hasPermissions = HasPermissions; HashSet existingItemFrames = new HashSet(); + bool refreshingBuyList = listBox == shoppingCrateBuyList; int totalPrice = 0; foreach (PurchasedItem item in items) { @@ -859,6 +860,7 @@ namespace Barotrauma { numInput.UserData = item; numInput.Enabled = hasPermissions; + numInput.MaxValueInt = GetMaxAvailable(item.ItemPrefab, refreshingBuyList ? StoreTab.Buy : StoreTab.Sell); } SetOwnedLabelText(itemFrame); SetItemFrameStatus(itemFrame, hasPermissions); @@ -873,7 +875,7 @@ namespace Barotrauma } suppressBuySell = false; - var price = listBox == shoppingCrateBuyList ? + var price = refreshingBuyList ? CurrentLocation.GetAdjustedItemBuyPrice(item.ItemPrefab, priceInfo: priceInfo) : CurrentLocation.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo); totalPrice += item.Quantity * price; @@ -884,7 +886,7 @@ namespace Barotrauma SortItems(listBox, SortingMethod.CategoryAsc); listBox.UpdateScrollBarSize(); - if (listBox == shoppingCrateBuyList) + if (refreshingBuyList) { buyTotal = totalPrice; if (IsBuying) { SetShoppingCrateTotalText(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs index d4c1d980d..409bb15fb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs @@ -12,7 +12,8 @@ namespace Barotrauma private const int submarinesPerPage = 4; private int currentPage = 1; private int pageCount; - private bool transferService, purchaseService, initialized; + private readonly bool transferService, purchaseService; + private bool initialized; private int deliveryFee; private string deliveryLocationName; @@ -27,12 +28,12 @@ namespace Barotrauma private int selectionIndicatorThickness; private GUIImage listBackground; - private List subsToShow; - private SubmarineDisplayContent[] submarineDisplays = new SubmarineDisplayContent[submarinesPerPage]; + private readonly List subsToShow; + private readonly SubmarineDisplayContent[] submarineDisplays = new SubmarineDisplayContent[submarinesPerPage]; private SubmarineInfo selectedSubmarine = null; private string purchaseAndSwitchText, purchaseOnlyText, deliveryText, currentSubText, deliveryFeeText, priceText, switchText, missingPreviewText, currencyShorthandText, currencyLongText; - private RectTransform parent; - private Action closeAction; + private readonly RectTransform parent; + private readonly Action closeAction; private Sprite pageIndicator; public static readonly string[] DeliveryTextVariables = new string[] { "[submarinename1]", "[location1]", "[location2]", "[submarinename2]", "[amount]", "[currencyname]" }; @@ -42,7 +43,7 @@ namespace Barotrauma private static readonly string[] notEnoughCreditsDeliveryTextVariables = new string[] { "[currencyname]", "[submarinename]", "[location1]", "[location2]" }; private static readonly string[] notEnoughCreditsPurchaseTextVariables = new string[] { "[currencyname]", "[submarinename]" }; - private string[] messageBoxOptions; + private readonly string[] messageBoxOptions; public const int DeliveryFeePerDistanceTravelled = 1000; public static bool ContentRefreshRequired = false; @@ -65,7 +66,7 @@ namespace Barotrauma public SubmarineSelection(bool transfer, Action closeAction, RectTransform parent) { - if (GameMain.GameSession.Campaign == null) return; + if (GameMain.GameSession.Campaign == null) { return; } transferService = transfer; purchaseService = !transfer; @@ -83,7 +84,7 @@ namespace Barotrauma messageBoxOptions = new string[2] { TextManager.Get("Yes") + " " + TextManager.Get("initiatevoting"), TextManager.Get("Cancel") }; } - if (Submarine.MainSub?.Info == null) return; + if (Submarine.MainSub?.Info == null) { return; } Initialize(); } @@ -184,8 +185,10 @@ namespace Barotrauma for (int i = 0; i < submarineDisplays.Length; i++) { - SubmarineDisplayContent submarineDisplayElement = new SubmarineDisplayContent(); - submarineDisplayElement.background = new GUIFrame(new RectTransform(new Vector2(1f / submarinesPerPage, 1f), submarineHorizontalGroup.RectTransform), style: null, new Color(8, 13, 19)); + SubmarineDisplayContent submarineDisplayElement = new SubmarineDisplayContent + { + background = new GUIFrame(new RectTransform(new Vector2(1f / submarinesPerPage, 1f), submarineHorizontalGroup.RectTransform), style: null, new Color(8, 13, 19)) + }; submarineDisplayElement.submarineImage = new GUIImage(new RectTransform(new Vector2(0.8f, 1f), submarineDisplayElement.background.RectTransform, Anchor.Center), null, true); submarineDisplayElement.middleTextBlock = new GUITextBlock(new RectTransform(new Vector2(0.8f, 1f), submarineDisplayElement.background.RectTransform, Anchor.Center), string.Empty, textAlignment: Alignment.Center); submarineDisplayElement.submarineName = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), submarineDisplayElement.background.RectTransform, Anchor.TopCenter, Pivot.TopCenter) { AbsoluteOffset = new Point(0, HUDLayoutSettings.Padding) }, string.Empty, textAlignment: Alignment.Center, font: GUI.SubHeadingFont); @@ -433,7 +436,7 @@ namespace Barotrauma private SubmarineInfo GetSubToDisplay(int index) { - if (subsToShow.Count <= index || index < 0) return null; + if (subsToShow.Count <= index || index < 0) { return null; } return subsToShow[index]; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs index 5f4ccab30..db3b4336a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs @@ -112,10 +112,10 @@ namespace Barotrauma { switch (selectedUpgradeTab) { - case UpgradeTab.Repairs: + case UpgradeTab.Repairs: SelectTab(UpgradeTab.Repairs); - break; - case UpgradeTab.Upgrade: + break; + case UpgradeTab.Upgrade: RefreshUpgradeList(); foreach (var itemPreview in itemPreviews) { @@ -123,7 +123,7 @@ namespace Barotrauma if (!(itemPreview.Value is GUIImage image)) { continue; } image.Sprite = itemPreview.Key.PendingItemSwap.UpgradePreviewSprite; } - break; + break; } } @@ -170,6 +170,11 @@ namespace Barotrauma // ReSharper disable once PossibleMultipleEnumeration UpdateCategoryIndicators(indicators, component, data.Prefabs, data.Category, campaign, drawnSubmarine, applicableCategories); } + var customizeButton = component.FindChild("customizebutton", true); + if (customizeButton != null) + { + customizeButton.Visible = HasSwappableItems(data.Category); + } } // reset the order first @@ -625,6 +630,20 @@ namespace Barotrauma frameChild.DefaultColor = frameChild.Color; frameChild.Color = Color.Transparent; + var weaponSwitchBg = new GUIButton(new RectTransform(new Vector2(0.65f), frameChild.RectTransform, Anchor.TopRight, scaleBasis: ScaleBasis.Smallest) + { RelativeOffset = new Vector2(0.04f, 0.0f) }, style: "WeaponSwitchTab") + { + Visible = false, + CanBeSelected = false, + UserData = "customizebutton" + }; + weaponSwitchBg.DefaultColor = weaponSwitchBg.Frame.DefaultColor = weaponSwitchBg.Color; + var weaponSwitchImg = new GUIImage(new RectTransform(new Vector2(0.7f), weaponSwitchBg.RectTransform, Anchor.Center), "WeaponSwitchIcon", scaleToFit: true) + { + CanBeFocused = false + }; + weaponSwitchImg.DefaultColor = weaponSwitchImg.Color; + /* UPGRADE CATEGORY * |--------------------------------------------------------| * | | @@ -670,6 +689,7 @@ namespace Barotrauma foreach (GUIComponent itemFrame in itemPreviews.Values) { itemFrame.OutlineColor = itemFrame.Color = previewWhite; + itemFrame.Children.ForEach(c => c.Color = itemFrame.Color); } return true; } @@ -679,6 +699,9 @@ namespace Barotrauma TrySelectCategory(prefabs, categoryData.Category, sub); } + var customizeCategoryButton = selectedUpgradeCategoryLayout?.FindChild("customizebutton", recursive: true) as GUIButton; + customizeCategoryButton?.OnClicked(customizeCategoryButton, customizeCategoryButton.UserData); + return true; }; } @@ -688,6 +711,16 @@ namespace Barotrauma private bool customizeTabOpen; + private static bool HasSwappableItems(UpgradeCategory category) + { + if (Submarine.MainSub == null) { return false; } + return Submarine.MainSub.GetItems(true).Any(i => + i.Prefab.SwappableItem != null && + !i.HiddenInGame && i.AllowSwapping && + (i.Prefab.SwappableItem.CanBeBought || ItemPrefab.Prefabs.Any(ip => ip.SwappableItem?.ReplacementOnUninstall == i.Prefab.Identifier)) && + Submarine.MainSub.IsEntityFoundOnThisSub(i, true) && category.ItemTags.Any(t => i.HasTag(t))); + } + private void SelectUpgradeCategory(List prefabs, UpgradeCategory category, Submarine submarine) { if (selectedUpgradeCategoryLayout == null) { return; } @@ -698,6 +731,7 @@ namespace Barotrauma foreach (GUIComponent itemFrame in itemPreviews.Values) { itemFrame.OutlineColor = itemFrame.Color = categoryFrames.Contains(itemFrame) ? GUI.Style.Orange : previewWhite; + itemFrame.Children.ForEach(c => c.Color = itemFrame.Color); } highlightWalls = category.IsWallUpgrade; @@ -706,11 +740,7 @@ namespace Barotrauma GUIFrame frame = new GUIFrame(rectT(1.0f, 0.4f, selectedUpgradeCategoryLayout)); GUIFrame paddedFrame = new GUIFrame(rectT(0.93f, 0.9f, frame, Anchor.Center), style: null); - bool hasSwappableItems = Submarine.MainSub.GetItems(true).Any(i => - i.Prefab.SwappableItem != null && - !i.HiddenInGame && i.AllowSwapping && - (i.Prefab.SwappableItem.CanBeBought || ItemPrefab.Prefabs.Any(ip => ip.SwappableItem?.ReplacementOnUninstall == i.Prefab.Identifier)) && - Submarine.MainSub.IsEntityFoundOnThisSub(i, true) && category.ItemTags.Any(t => i.HasTag(t))); + bool hasSwappableItems = HasSwappableItems(category); float listHeight = hasSwappableItems ? 0.9f : 1.0f; @@ -725,12 +755,19 @@ namespace Barotrauma { GUILayoutGroup buttonLayout = new GUILayoutGroup(rectT(1.0f, 0.1f, paddedFrame, anchor: Anchor.TopLeft), isHorizontal: true); + GUIButton customizeButton = new GUIButton(rectT(0.5f, 1f, buttonLayout), text: TextManager.Get("uicategory.customize"), style: "GUITabButton") + { + UserData = "customizebutton" + }; + new GUIImage(new RectTransform(new Vector2(1.0f, 0.75f), customizeButton.RectTransform, Anchor.CenterLeft, scaleBasis: ScaleBasis.Smallest) { RelativeOffset = new Vector2(0.015f, 0.0f) }, "WeaponSwitchIcon", scaleToFit: true); + customizeButton.TextBlock.RectTransform.RelativeSize = new Vector2(0.7f, 1.0f); + GUIButton upgradeButton = new GUIButton(rectT(0.5f, 1f, buttonLayout), text: TextManager.Get("uicategory.upgrades"), style: "GUITabButton") { Selected = true }; - GUIButton customizeButton = new GUIButton(rectT(0.5f, 1f, buttonLayout), text: TextManager.Get("uicategory.customize"), style: "GUITabButton"); + GUITextBlock.AutoScaleAndNormalize(upgradeButton.TextBlock, customizeButton.TextBlock); upgradeButton.OnClicked = delegate { @@ -742,6 +779,7 @@ namespace Barotrauma foreach (GUIComponent itemFrame in itemPreviews.Values) { itemFrame.OutlineColor = itemFrame.Color = categoryFrames.Contains(itemFrame) ? GUI.Style.Orange : previewWhite; + itemFrame.Children.ForEach(c => c.Color = itemFrame.Color); } return true; }; @@ -754,7 +792,6 @@ namespace Barotrauma CreateSwappableItemList(prefabList, category, submarine); return true; }; - } CreateUpgradePrefabList(prefabList, category, prefabs, submarine); @@ -779,23 +816,25 @@ namespace Barotrauma { parent.Content.ClearChildren(); currentUpgradeCategory = category; - IEnumerable availableReplacements = MapEntityPrefab.List.Where(p => - p is ItemPrefab itemPrefab && - category.ItemTags.Any(t => itemPrefab.Tags.Contains(t)) && - (itemPrefab.SwappableItem?.CanBeBought ?? false)).Cast(); var entitiesOnSub = submarine.GetItems(true).Where(i => submarine.IsEntityFoundOnThisSub(i, true) && !i.HiddenInGame && i.AllowSwapping && category.ItemTags.Any(t => i.HasTag(t))).ToList(); int slotIndex = 0; foreach (Item item in entitiesOnSub) { slotIndex++; - CreateSwappableItemSlideDown(parent, slotIndex, item, availableReplacements); + CreateSwappableItemSlideDown(parent, slotIndex, item, submarine); } } - private void CreateSwappableItemSlideDown(GUIListBox parent, int slotIndex, Item item, IEnumerable availableReplacements) + private void CreateSwappableItemSlideDown(GUIListBox parent, int slotIndex, Item item, Submarine submarine) { - if (Campaign == null) { return; } + if (Campaign == null || submarine == null) { return; } + + IEnumerable availableReplacements = MapEntityPrefab.List.Where(p => + p is ItemPrefab itemPrefab && + itemPrefab.SwappableItem != null && + itemPrefab.SwappableItem.CanBeBought && + itemPrefab.SwappableItem.SwapIdentifier.Equals(item.Prefab.SwappableItem.SwapIdentifier, StringComparison.OrdinalIgnoreCase)).Cast(); var currentOrPending = item.PendingItemSwap ?? item.Prefab; @@ -833,7 +872,7 @@ namespace Barotrauma frames.Add(CreateUpgradeEntry(rectT(1f, 0.25f, parent.Content), currentOrPending.UpgradePreviewSprite, TextManager.GetWithVariable(item.PendingItemSwap != null ? "upgrades.pendingitem" : "upgrades.installeditem", "[itemname]", currentOrPending.Name), currentOrPending.Description, - 0, null, addBuyButton: canUninstall, addProgressBar: false, buttonStyle: "StoreRemoveFromCrateButton")); + 0, null, addBuyButton: canUninstall, addProgressBar: false, buttonStyle: "WeaponUninstallButton")); if (canUninstall && frames.Last().FindChild(c => c is GUIButton, recursive: true) is GUIButton refundButton) { @@ -876,7 +915,7 @@ namespace Barotrauma price, replacement, addBuyButton: true, addProgressBar: false, - buttonStyle: isPurchased ? "UpgradeBuyButton" : "StoreAddToCrateButton")); + buttonStyle: isPurchased ? "WeaponInstallButton" : "StoreAddToCrateButton")); if (!(frames.Last().FindChild(c => c is GUIButton, recursive: true) is GUIButton buyButton)) { continue; } if (Campaign.Money >= price) @@ -1234,9 +1273,11 @@ namespace Barotrauma { if (selectedUpgradeCategoryLayout != null) { - if (selectedUpgradeCategoryLayout.FindChild(c => c.UserData as Item == HoveredItem, recursive: true) is GUIButton itemElement && !itemElement.Selected) + if (selectedUpgradeCategoryLayout.FindChild(c => c.UserData as Item == HoveredItem, recursive: true) is GUIButton itemElement) { - itemElement.OnClicked(itemElement, itemElement.UserData); + if (!itemElement.Selected) { itemElement.OnClicked(itemElement, itemElement.UserData); } + //TODO: enable this if/when we make ScrollToElement work with child elements of different sizes + //(itemElement.Parent?.Parent?.Parent as GUIListBox)?.ScrollToElement(itemElement); } } } @@ -1343,6 +1384,15 @@ namespace Barotrauma HoverCursor = CursorState.Hand, SpriteEffects = item.Rotation > 90.0f && item.Rotation < 270.0f ? SpriteEffects.FlipVertically : SpriteEffects.None }; + if (item.Prefab.SwappableItem != null) + { + new GUIImage(new RectTransform(new Vector2(0.8f), itemFrame.RectTransform, Anchor.TopLeft) { RelativeOffset = new Vector2(-0.2f) }, "WeaponSwitchIcon.DropShadow", scaleToFit: true) + { + SelectedColor = GUI.Style.Orange, + Color = previewWhite, + CanBeFocused = false + }; + } } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index 6672be5c8..0e65c1d8c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -1287,18 +1287,28 @@ namespace Barotrauma private int TryAdjustIndex(int amount) { - int index = Character.Controlled == null ? 0 : - crewList.Content.GetChildIndex(crewList.Content.GetChildByUserData(Character.Controlled)) + amount; + if (Character.Controlled == null) { return 0; } + + int currentIndex = crewList.Content.GetChildIndex(crewList.Content.GetChildByUserData(Character.Controlled)); + if (currentIndex == -1) { return 0; } + int lastIndex = crewList.Content.CountChildren - 1; - if (index > lastIndex) + + int index = currentIndex + amount; + for (int i = 0; i < crewList.Content.CountChildren; i++) { - index = 0; + if (index > lastIndex) { index = 0; } + if (index < 0) { index = lastIndex; } + + if ((crewList.Content.GetChild(index)?.UserData as Character)?.IsOnPlayerTeam ?? false) + { + return index; + } + + index += amount; } - if (index < 0) - { - index = lastIndex; - } - return index; + + return 0; } partial void UpdateProjectSpecific(float deltaTime) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index 5c65ea608..c51a4a68d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -445,9 +445,13 @@ namespace Barotrauma { Submarine.MainSub = leavingSub; GameMain.GameSession.Submarine = leavingSub; + GameMain.GameSession.SubmarineInfo = leavingSub.Info; + leavingSub.Info.FilePath = System.IO.Path.Combine(SaveUtil.TempPath, leavingSub.Info.Name + ".sub"); var subsToLeaveBehind = GetSubsToLeaveBehind(leavingSub); + GameMain.GameSession.OwnedSubmarines.Add(leavingSub.Info); foreach (Submarine sub in subsToLeaveBehind) { + GameMain.GameSession.OwnedSubmarines.RemoveAll(s => s != leavingSub.Info && s.Name == sub.Info.Name); MapEntity.mapEntityList.RemoveAll(e => e.Submarine == sub && e is LinkedSubmarine); LinkedSubmarine.CreateDummy(leavingSub, sub); } @@ -559,7 +563,7 @@ namespace Barotrauma } #if DEBUG - if (PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.R)) + if (GUI.KeyboardDispatcher.Subscriber == null && PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.M)) { if (GUIMessageBox.MessageBoxes.Any()) { GUIMessageBox.MessageBoxes.Remove(GUIMessageBox.MessageBoxes.Last()); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index b1beafe96..89a6b1210 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -537,9 +537,12 @@ namespace Barotrauma } List hideSubInventories = new List(); + //remove highlighted subinventory slots that can no longer be accessed highlightedSubInventorySlots.RemoveWhere(s => s.ParentInventory == this && ((s.SlotIndex < 0 || s.SlotIndex >= slots.Length || slots[s.SlotIndex] == null) || (Character.Controlled != null && !Character.Controlled.CanAccessInventory(s.Inventory)))); + //remove highlighted subinventory slots that refer to items no longer in this inventory + highlightedSubInventorySlots.RemoveWhere(s => s.Item != null && s.ParentInventory == this && s.Item.ParentInventory != this); foreach (var highlightedSubInventorySlot in highlightedSubInventorySlots) { if (highlightedSubInventorySlot.ParentInventory == this) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index f99a471ca..289c88ec2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -611,5 +611,6 @@ namespace Barotrauma.Items.Components } OnResolutionChanged(); } + public virtual void AddTooltipInfo(ref string description) { } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index a92ec14dd..dbf89512b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -1094,7 +1094,10 @@ namespace Barotrauma.Items.Components if (!dockingPort.Item.Submarine.ShowSonarMarker && dockingPort.Item.Submarine != item.Submarine && !dockingPort.Item.Submarine.Info.IsOutpost) { continue; } //don't show the docking ports of the opposing team on the sonar - if (item.Submarine != null && item.Submarine != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle && dockingPort.Item.Submarine.Info.Type != SubmarineType.Outpost) + if (item.Submarine != null && + item.Submarine != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle && + dockingPort.Item.Submarine != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle && + dockingPort.Item.Submarine.Info.Type != SubmarineType.Outpost) { // specifically checking for friendlyNPC seems more logical here if (dockingPort.Item.Submarine.TeamID != item.Submarine.TeamID && dockingPort.Item.Submarine.TeamID != CharacterTeamType.FriendlyNPC) { continue; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs index ce86894a0..f83259306 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs @@ -367,9 +367,9 @@ namespace Barotrauma.Items.Components //a wire has been selected -> check if we should start dragging one of the nodes float nodeSelectDist = 10, sectionSelectDist = 5; highlightedNodeIndex = null; - if (MapEntity.SelectedList.Count == 1 && MapEntity.SelectedList[0] is Item) + if (MapEntity.SelectedList.Count == 1 && MapEntity.SelectedList.FirstOrDefault() is Item selectedItem) { - Wire selectedWire = ((Item)MapEntity.SelectedList[0]).GetComponent(); + Wire selectedWire = selectedItem.GetComponent(); if (selectedWire != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs index 744067877..581fd311e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs @@ -267,7 +267,7 @@ namespace Barotrauma.Items.Components } else if (chargeSoundChannel != null) { - chargeSoundChannel.FrequencyMultiplier = MathHelper.Lerp(1f, 2f, chargeRatio); + chargeSoundChannel.FrequencyMultiplier = MathHelper.Lerp(0.5f, 1.5f, chargeRatio); } break; default: @@ -363,13 +363,13 @@ namespace Barotrauma.Items.Components float chargeRatio = currentChargeTime / MaxChargeTime; - foreach (Tuple chargeSprite in chargeSprites) + foreach ((Sprite chargeSprite, Vector2 position) in chargeSprites) { - chargeSprite.Item1?.Draw(spriteBatch, - drawPos - MathUtils.RotatePoint(new Vector2(chargeSprite.Item2.X * chargeRatio, chargeSprite.Item2.Y * chargeRatio) * item.Scale, rotation + MathHelper.PiOver2), + chargeSprite?.Draw(spriteBatch, + drawPos - MathUtils.RotatePoint(new Vector2(position.X * chargeRatio, position.Y * chargeRatio) * item.Scale, rotation + MathHelper.PiOver2), item.SpriteColor, rotation + MathHelper.PiOver2, item.Scale, - SpriteEffects.None, item.SpriteDepth + (chargeSprite.Item1.Depth - item.Sprite.Depth)); + SpriteEffects.None, item.SpriteDepth + (chargeSprite.Depth - item.Sprite.Depth)); } int spinningBarrelCount = spinningBarrelSprites.Count; @@ -380,7 +380,7 @@ namespace Barotrauma.Items.Components Sprite spinningBarrel = spinningBarrelSprites[i]; float barrelCirclePosition = (MaxCircle * i / spinningBarrelCount + currentBarrelSpin) % MaxCircle; - float newDepth = spinningBarrel.Depth + (barrelCirclePosition > HalfCircle ? -0.001f : 0.001f); + float newDepth = item.SpriteDepth + (spinningBarrel.Depth - item.Sprite.Depth) + (barrelCirclePosition > HalfCircle ? 0.0f : 0.001f); float barrelColorPosition = (barrelCirclePosition + QuarterCircle) % MaxCircle; float colorOffset = Math.Abs(barrelColorPosition - HalfCircle) / HalfCircle; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs new file mode 100644 index 000000000..5aa13a52b --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs @@ -0,0 +1,50 @@ +using System; +using System.Linq; + +namespace Barotrauma.Items.Components +{ + partial class Wearable + { + private void GetDamageModifierText(ref string description, float damageMultiplier, string afflictionIdentifier) + { + string colorStr = XMLExtensions.ColorToString(GUI.Style.Green); + description += $"\n ‖color:{colorStr}‖-{Math.Round((1 - damageMultiplier) * 100)}%‖color:end‖ {TextManager.Get("AfflictionName." + afflictionIdentifier, true) ?? afflictionIdentifier}"; + } + + public override void AddTooltipInfo(ref string description) + { + if (damageModifiers.Any(d => d.DamageMultiplier != 1f) || SkillModifiers.Any()) + { + description += "\n"; + } + + if (damageModifiers.Any()) + { + foreach (DamageModifier damageModifier in damageModifiers) + { + if (damageModifier.DamageMultiplier == 1f) + { + continue; + } + + foreach (string afflictionIdentifier in damageModifier.ParsedAfflictionIdentifiers) + { + GetDamageModifierText(ref description, damageModifier.DamageMultiplier, afflictionIdentifier); + } + foreach (string afflictionIdentifier in damageModifier.ParsedAfflictionTypes) + { + GetDamageModifierText(ref description, damageModifier.DamageMultiplier, afflictionIdentifier); + } + } + } + if (SkillModifiers.Any()) + { + foreach (var skillModifier in SkillModifiers) + { + string colorStr = XMLExtensions.ColorToString(GUI.Style.Green); + description += $"\n ‖color:{colorStr}‖+{skillModifier.Value}‖color:end‖ {TextManager.Get("SkillName." + skillModifier.Key, true) ?? skillModifier.Key}"; + } + } + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index ce48fbe7c..6c0975078 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -296,6 +296,12 @@ namespace Barotrauma } } } + + foreach (ItemComponent component in item.Components) + { + component.AddTooltipInfo(ref description); + } + if (item.Prefab.ShowContentsInTooltip && item.OwnInventory != null) { foreach (string itemName in item.OwnInventory.AllItems.Select(it => it.Name).Distinct()) @@ -950,6 +956,15 @@ namespace Barotrauma { return CursorState.Hand; } + var container = item?.GetComponent(); + if (container == null) { continue; } + if (container.Inventory.visualSlots != null) + { + if (container.Inventory.visualSlots.Any(slot => slot.IsHighlighted)) + { + return CursorState.Hand; + } + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index 0e99ca9d8..8c90d177f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -1586,11 +1586,20 @@ namespace Barotrauma } } - var item = new Item(itemPrefab, pos, sub, id: itemId) + Item item = null; + try { - SpawnedInOutpost = spawnedInOutpost, - AllowStealing = allowStealing - }; + item = new Item(itemPrefab, pos, sub, id: itemId) + { + SpawnedInOutpost = spawnedInOutpost, + AllowStealing = allowStealing + }; + } + catch (Exception e) + { + DebugConsole.ThrowError($"Failed to spawn item {itemPrefab.Name}", e); + throw; + } if (item.body != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Explosion.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Explosion.cs index f8a8e97cb..308010cac 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Explosion.cs @@ -71,7 +71,7 @@ namespace Barotrauma if (sparks) { GameMain.ParticleManager.CreateParticle("spark", worldPosition, - Rand.Vector(Rand.Range(500.0f, 800.0f)), 0.0f, hull); + Rand.Vector(Rand.Range(1200.0f, 2400.0f)), 0.0f, hull); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs index a083a5e89..2627bec37 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs @@ -29,20 +29,14 @@ namespace Barotrauma public static bool SelectionChanged; //which entities have been selected for editing - private static List selectedList = new List(); - public static List SelectedList - { - get - { - return selectedList; - } - } - private static List copiedList = new List(); + public static HashSet SelectedList { get; private set; } = new HashSet(); + + public static List CopiedList = new List(); private static List highlightedList = new List(); // Test feature. Not yet saved. - public static Dictionary> SelectionGroups { get; private set; } = new Dictionary>(); + public static Dictionary> SelectionGroups { get; private set; } = new Dictionary>(); private static float highlightTimer; @@ -78,26 +72,12 @@ namespace Barotrauma } } - public virtual bool SelectableInEditor - { - get { return true; } - } + public virtual bool SelectableInEditor => true; - public static bool SelectedAny - { - get { return selectedList.Count > 0; } - } + public static bool SelectedAny => SelectedList.Count > 0; - public static IEnumerable CopiedList - { - get { return copiedList; } - } + public bool IsSelected => SelectedList.Contains(this); - public bool IsSelected - { - get { return selectedList.Contains(this); } - } - public bool IsIncludedInSelection { get; set; } public virtual bool IsVisible(Rectangle worldView) @@ -131,7 +111,10 @@ namespace Barotrauma { if (resizing) { - if (selectedList.Count == 0) resizing = false; + if (!SelectedAny) + { + resizing = false; + } return; } @@ -159,19 +142,19 @@ namespace Barotrauma if (MapEntityPrefab.Selected != null) { selectionPos = Vector2.Zero; - selectedList.Clear(); + SelectedList.Clear(); return; } if (GUI.KeyboardDispatcher.Subscriber == null) { if (PlayerInput.KeyHit(Keys.Delete)) { - if (selectedList.Any()) + if (SelectedAny) { - SubEditorScreen.StoreCommand(new AddOrDeleteCommand(selectedList, true)); + SubEditorScreen.StoreCommand(new AddOrDeleteCommand(new List(SelectedList), true)); } - selectedList.ForEach(e => { if (!e.Removed) { e.Remove(); } }); - selectedList.Clear(); + SelectedList.ForEach(e => { if (!e.Removed) { e.Remove(); } }); + SelectedList.Clear(); } if (PlayerInput.IsCtrlDown()) @@ -180,7 +163,7 @@ namespace Barotrauma if (PlayerInput.KeyHit(Keys.D)) { bool terminate = false; - foreach (MapEntity entity in selectedList) + foreach (MapEntity entity in SelectedList) { if (entity is Item item && item.GetComponent() is { } planter) { @@ -203,11 +186,11 @@ namespace Barotrauma #endif if (PlayerInput.KeyHit(Keys.C)) { - Copy(selectedList); + Copy(SelectedList.ToList()); } else if (PlayerInput.KeyHit(Keys.X)) { - Cut(selectedList); + Cut(SelectedList.ToList()); } else if (PlayerInput.KeyHit(Keys.V)) { @@ -215,21 +198,21 @@ namespace Barotrauma } else if (PlayerInput.KeyHit(Keys.G)) { - if (selectedList.Any()) + if (SelectedList.Any()) { - if (SelectionGroups.ContainsKey(selectedList.Last())) + if (SelectionGroups.ContainsKey(SelectedList.Last())) { // Ungroup all selected - selectedList.ForEach(e => SelectionGroups.Remove(e)); + SelectedList.ForEach(e => SelectionGroups.Remove(e)); } else { - foreach (var entity in selectedList) + foreach (var entity in SelectedList) { // Remove the old group, if any SelectionGroups.Remove(entity); // Create a group that can be accessed with any member - SelectionGroups.Add(entity, selectedList); + SelectionGroups.Add(entity, SelectedList); } } } @@ -279,7 +262,7 @@ namespace Barotrauma Vector2 nudge = GetNudgeAmount(); if (nudge != Vector2.Zero) { - foreach (MapEntity entityToNudge in selectedList) { entityToNudge.Move(nudge); } + foreach (MapEntity entityToNudge in SelectedList) { entityToNudge.Move(nudge); } } } else @@ -292,7 +275,7 @@ namespace Barotrauma //started moving selected entities if (startMovingPos != Vector2.Zero) { - Item targetContainer = GetPotentialContainer(position, selectedList); + Item targetContainer = GetPotentialContainer(position, SelectedList); if (targetContainer != null) { targetContainer.IsHighlighted = true; } @@ -315,16 +298,16 @@ namespace Barotrauma //clone if (PlayerInput.IsCtrlDown()) { - var clones = Clone(selectedList).Where(c => c != null).ToList(); - selectedList = clones; - selectedList.ForEach(c => c.Move(moveAmount)); - SubEditorScreen.StoreCommand(new AddOrDeleteCommand(clones, false)); + HashSet clones = Clone(SelectedList.ToList()).Where(c => c != null).ToHashSet(); + SelectedList = clones; + SelectedList.ForEach(c => c.Move(moveAmount)); + SubEditorScreen.StoreCommand(new AddOrDeleteCommand(new List(clones), false)); } else // move { - var oldRects = selectedList.Select(e => e.Rect).ToList(); + var oldRects = SelectedList.Select(e => e.Rect).ToList(); List deposited = new List(); - foreach (MapEntity e in selectedList) + foreach (MapEntity e in SelectedList) { e.Move(moveAmount); @@ -342,14 +325,14 @@ namespace Barotrauma } } - SubEditorScreen.StoreCommand(new TransformCommand(new List(selectedList),selectedList.Select(entity => entity.Rect).ToList(), oldRects, false)); + SubEditorScreen.StoreCommand(new TransformCommand(new List(SelectedList),SelectedList.Select(entity => entity.Rect).ToList(), oldRects, false)); if (deposited.Any() && deposited.Any(entity => entity is Item)) { var depositedItems = deposited.Where(entity => entity is Item).Cast().ToList(); SubEditorScreen.StoreCommand(new InventoryPlaceCommand(targetContainer.OwnInventory, depositedItems, false)); } - deposited.ForEach(entity => { selectedList.Remove(entity); }); + deposited.ForEach(entity => { SelectedList.Remove(entity); }); } } startMovingPos = Vector2.Zero; @@ -367,7 +350,7 @@ namespace Barotrauma entity.IsIncludedInSelection = false; } - List newSelection = new List();// FindSelectedEntities(selectionPos, selectionSize); + HashSet newSelection = new HashSet();// FindSelectedEntities(selectionPos, selectionSize); if (Math.Abs(selectionSize.X) > Submarine.GridSize.X || Math.Abs(selectionSize.Y) > Submarine.GridSize.Y) { newSelection = FindSelectedEntities(selectionPos, selectionSize); @@ -376,9 +359,13 @@ namespace Barotrauma { if (highLightedEntity != null) { - if (SelectionGroups.TryGetValue(highLightedEntity, out List group)) + if (SelectionGroups.TryGetValue(highLightedEntity, out HashSet group)) { - newSelection.AddRange(group); + foreach (MapEntity entity in group.Where(e => !newSelection.Contains(e))) + { + newSelection.Add(entity); + } + foreach (MapEntity entity in group) { entity.IsIncludedInSelection = true; @@ -398,7 +385,7 @@ namespace Barotrauma { foreach (MapEntity e in newSelection) { - if (selectedList.Contains(e)) + if (SelectedList.Contains(e)) { RemoveSelection(e); } @@ -410,7 +397,7 @@ namespace Barotrauma } else { - selectedList = new List(newSelection); + SelectedList = new HashSet(newSelection); //selectedList.Clear(); //newSelection.ForEach(e => AddSelection(e)); foreach (var entity in newSelection) @@ -419,23 +406,23 @@ namespace Barotrauma onGapFound: (door, gap) => { door.RefreshLinkedGap(); - if (!selectedList.Contains(gap)) + if (!SelectedList.Contains(gap)) { - selectedList.Add(gap); + SelectedList.Add(gap); } }, onDoorFound: (door, gap) => { - if (!selectedList.Contains(door.Item)) + if (!SelectedList.Contains(door.Item)) { - selectedList.Add(door.Item); + SelectedList.Add(door.Item); } }); } } //select wire if both items it's connected to are selected - var selectedItems = selectedList.Where(e => e is Item).Cast().ToList(); + var selectedItems = SelectedList.Where(e => e is Item).Cast().ToList(); foreach (Item item in selectedItems) { if (item.Connections == null) continue; @@ -443,11 +430,11 @@ namespace Barotrauma { foreach (Wire w in c.Wires) { - if (w == null || selectedList.Contains(w.Item)) continue; + if (w == null || SelectedList.Contains(w.Item)) continue; - if (w.OtherConnection(c) != null && selectedList.Contains(w.OtherConnection(c).Item)) + if (w.OtherConnection(c) != null && SelectedList.Contains(w.OtherConnection(c).Item)) { - selectedList.Add(w.Item); + SelectedList.Add(w.Item); } } } @@ -471,7 +458,7 @@ namespace Barotrauma (highlightedListBox == null || (GUI.MouseOn != highlightedListBox && !highlightedListBox.IsParentOf(GUI.MouseOn)))) { //if clicking a selected entity, start moving it - foreach (MapEntity e in selectedList) + foreach (MapEntity e in SelectedList) { if (e.IsMouseOn(position)) startMovingPos = position; } @@ -519,7 +506,7 @@ namespace Barotrauma return ReplacedBy?.GetReplacementOrThis() ?? this; } - public static Item GetPotentialContainer(Vector2 position, List entities = null) + public static Item GetPotentialContainer(Vector2 position, HashSet entities = null) { Item targetContainer = null; bool isShiftDown = PlayerInput.IsShiftDown(); @@ -654,7 +641,7 @@ namespace Barotrauma if (PlayerInput.IsCtrlDown() && !wiringMode) { - if (selectedList.Contains(entity)) + if (SelectedList.Contains(entity)) { RemoveSelection(entity); } @@ -673,56 +660,60 @@ namespace Barotrauma public static void AddSelection(MapEntity entity) { - if (selectedList.Contains(entity)) { return; } - selectedList.Add(entity); + if (SelectedList.Contains(entity)) { return; } + SelectedList.Add(entity); HandleDoorGapLinks(entity, onGapFound: (door, gap) => { door.RefreshLinkedGap(); - if (!selectedList.Contains(gap)) + if (!SelectedList.Contains(gap)) { - selectedList.Add(gap); + SelectedList.Add(gap); } }, onDoorFound: (door, gap) => { - if (!selectedList.Contains(door.Item)) + if (!SelectedList.Contains(door.Item)) { - selectedList.Add(door.Item); + SelectedList.Add(door.Item); } }); } private static void HandleDoorGapLinks(MapEntity entity, Action onGapFound, Action onDoorFound) { - if (entity is Item i) + switch (entity) { - var door = i.GetComponent(); - if (door != null) + case Item i: { - var gap = door.LinkedGap; + var door = i.GetComponent(); + var gap = door?.LinkedGap; if (gap != null) { onGapFound(door, gap); } + + break; } - } - else if (entity is Gap gap) - { - var door = gap.ConnectedDoor; - if (door != null) + case Gap gap: { - onDoorFound(door, gap); + var door = gap.ConnectedDoor; + if (door != null) + { + onDoorFound(door, gap); + } + + break; } } } public static void RemoveSelection(MapEntity entity) { - selectedList.Remove(entity); + SelectedList.Remove(entity); HandleDoorGapLinks(entity, - onGapFound: (door, gap) => selectedList.Remove(gap), - onDoorFound: (door, gap) => selectedList.Remove(door.Item)); + onGapFound: (door, gap) => SelectedList.Remove(gap), + onDoorFound: (door, gap) => SelectedList.Remove(door.Item)); } static partial void UpdateAllProjSpecific(float deltaTime) @@ -767,7 +758,7 @@ namespace Barotrauma //started moving the selected entities if (Math.Abs(moveAmount.X) >= Submarine.GridSize.X || Math.Abs(moveAmount.Y) >= Submarine.GridSize.Y || isShiftDown) { - foreach (MapEntity e in selectedList) + foreach (MapEntity e in SelectedList) { SpriteEffects spriteEffects = SpriteEffects.None; switch (e) @@ -865,8 +856,8 @@ namespace Barotrauma } } FilteredSelectedList.Clear(); - if (selectedList.Count == 0) return; - foreach (var e in selectedList) + if (SelectedList.Count == 0) return; + foreach (var e in SelectedList) { if (e is Gap gap && gap.ConnectedDoor != null) { continue; } FilteredSelectedList.Add(e); @@ -885,15 +876,19 @@ namespace Barotrauma { if (PlayerInput.KeyHit(Keys.N)) { - float minX = selectedList[0].WorldRect.X, maxX = selectedList[0].WorldRect.Right; - for (int i = 0; i < selectedList.Count; i++) + MapEntity firstSelected = SelectedList.First(); + + float minX = firstSelected.WorldRect.X, + maxX = firstSelected.WorldRect.Right; + + foreach (MapEntity entity in SelectedList) { - minX = Math.Min(minX, selectedList[i].WorldRect.X); - maxX = Math.Max(maxX, selectedList[i].WorldRect.Right); + minX = Math.Min(minX, entity.WorldRect.X); + maxX = Math.Max(maxX, entity.WorldRect.Right); } float centerX = (minX + maxX) / 2.0f; - foreach (MapEntity me in selectedList) + foreach (MapEntity me in SelectedList) { me.FlipX(false); me.Move(new Vector2((centerX - me.WorldPosition.X) * 2.0f, 0.0f)); @@ -901,15 +896,20 @@ namespace Barotrauma } else if (PlayerInput.KeyHit(Keys.M)) { - float minY = selectedList[0].WorldRect.Y - selectedList[0].WorldRect.Height, maxY = selectedList[0].WorldRect.Y; - for (int i = 0; i < selectedList.Count; i++) + MapEntity firstSelected = SelectedList.First(); + + float minY = firstSelected.WorldRect.Y - firstSelected.WorldRect.Height, + maxY = firstSelected.WorldRect.Y; + + foreach (MapEntity entity in SelectedList) { - minY = Math.Min(minY, selectedList[i].WorldRect.Y - selectedList[i].WorldRect.Height); - maxY = Math.Max(maxY, selectedList[i].WorldRect.Y); + + minY = Math.Min(minY, entity.WorldRect.Y - entity.WorldRect.Height); + maxY = Math.Max(maxY, entity.WorldRect.Y); } float centerY = (minY + maxY) / 2.0f; - foreach (MapEntity me in selectedList) + foreach (MapEntity me in SelectedList) { me.FlipY(false); me.Move(new Vector2(0.0f, (centerY - me.WorldPosition.Y) * 2.0f)); @@ -920,19 +920,20 @@ namespace Barotrauma public static void DrawEditor(SpriteBatch spriteBatch, Camera cam) { - if (selectedList.Count == 1) + if (SelectedList.Count == 1) { - selectedList[0].DrawEditing(spriteBatch, cam); - if (selectedList[0].ResizeHorizontal || selectedList[0].ResizeVertical) + MapEntity firstSelected = SelectedList.First(); + firstSelected.DrawEditing(spriteBatch, cam); + if (firstSelected.ResizeHorizontal || firstSelected.ResizeVertical) { - selectedList[0].DrawResizing(spriteBatch, cam); + firstSelected.DrawResizing(spriteBatch, cam); } } } public static void DeselectAll() { - selectedList.Clear(); + SelectedList.Clear(); } public static void SelectEntity(MapEntity entity) @@ -967,10 +968,10 @@ namespace Barotrauma public static void Paste(Vector2 position) { - if (copiedList.Count == 0) { return; } + if (CopiedList.Count == 0) { return; } List prevEntities = new List(mapEntityList); - Clone(copiedList); + Clone(CopiedList); var clones = mapEntityList.Except(prevEntities).ToList(); var nonWireClones = clones.Where(c => !(c is Item item) || item.GetComponent() == null); @@ -982,8 +983,8 @@ namespace Barotrauma Vector2 moveAmount = Submarine.VectorToWorldGrid(position - center); - selectedList = new List(clones); - foreach (MapEntity clone in selectedList) + SelectedList = new HashSet(clones); + foreach (MapEntity clone in SelectedList) { clone.Move(moveAmount); clone.Submarine = Submarine.MainSub; @@ -999,7 +1000,7 @@ namespace Barotrauma { List prevEntities = new List(mapEntityList); - copiedList = Clone(entities); + CopiedList = Clone(entities); //find all new entities created during cloning var newEntities = mapEntityList.Except(prevEntities).ToList(); @@ -1172,9 +1173,9 @@ namespace Barotrauma /// /// Find entities whose rect intersects with the "selection rect" /// - public static List FindSelectedEntities(Vector2 pos, Vector2 size) + public static HashSet FindSelectedEntities(Vector2 pos, Vector2 size) { - List foundEntities = new List(); + HashSet foundEntities = new HashSet(); Rectangle selectionRect = Submarine.AbsRect(pos, size); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs index f3235e76d..956c3e104 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs @@ -26,8 +26,6 @@ namespace Barotrauma.Particles private float angularVelocity; - private Vector2 dragVec = Vector2.Zero; - private float dragWait = 0; private float collisionIgnoreTimer = 0; private Vector2 size; @@ -35,6 +33,7 @@ namespace Barotrauma.Particles private Color color; private bool changeColor; + private bool UseMiddleColor; private int spriteIndex; @@ -112,9 +111,7 @@ namespace Barotrauma.Particles animState = 0; animFrame = 0; - dragWait = 0; - dragVec = Vector2.Zero; - + currentHull = Hull.FindHull(position, hullGuess); size = prefab.StartSizeMin + (prefab.StartSizeMax - prefab.StartSizeMin) * Rand.Range(0.0f, 1.0f); @@ -144,10 +141,21 @@ namespace Barotrauma.Particles angularVelocity = Rand.Range(prefab.AngularVelocityMinRad, prefab.AngularVelocityMaxRad); - totalLifeTime = prefab.LifeTime; - lifeTime = prefab.LifeTime; + + if (prefab.LifeTimeMin <= 0.0f) + { + totalLifeTime = prefab.LifeTime; + lifeTime = prefab.LifeTime; + } + else + { + totalLifeTime = Rand.Range(prefab.LifeTimeMin, prefab.LifeTime); + lifeTime = totalLifeTime; + } + startDelay = Rand.Range(prefab.StartDelayMin, prefab.StartDelayMax); + UseMiddleColor = prefab.UseMiddleColor; color = prefab.StartColor; changeColor = prefab.StartColor != prefab.EndColor; ColorMultiplier = Vector4.One; @@ -243,13 +251,27 @@ namespace Barotrauma.Particles } size.X += sizeChange.X * deltaTime; - size.Y += sizeChange.Y * deltaTime; + size.Y += sizeChange.Y * deltaTime; - if (changeColor) + if (UseMiddleColor) { - color = Color.Lerp(prefab.EndColor, prefab.StartColor, lifeTime / prefab.LifeTime); + if (lifeTime > totalLifeTime * 0.5f) + { + color = Color.Lerp(prefab.MiddleColor, prefab.StartColor, (lifeTime / totalLifeTime - 0.5f) * 2.0f); + } + else + { + color = Color.Lerp(prefab.EndColor, prefab.MiddleColor, lifeTime / totalLifeTime * 2.0f); + } } - + else + { + if (changeColor) + { + color = Color.Lerp(prefab.EndColor, prefab.StartColor, lifeTime / totalLifeTime); + } + } + if (prefab.Sprites[spriteIndex] is SpriteSheet) { animState += deltaTime; @@ -399,29 +421,24 @@ namespace Barotrauma.Particles private void ApplyDrag(float dragCoefficient, float deltaTime) { - if (velocity.LengthSquared() < dragVec.LengthSquared()) + + float speed = velocity.Length(); + velocity /= speed; + + float drag = speed * speed * dragCoefficient * 0.01f * deltaTime; + if (drag > speed) { velocity = Vector2.Zero; - return; } - if (Math.Abs(velocity.X) < 0.0001f && Math.Abs(velocity.Y) < 0.0001f) return; - - //TODO: some better way to handle particle drag - //this doesn't work that well because the drag vector is only updated every 0.5 seconds, allowing the particle to accelerate way more than it should - //(e.g. a falling particle can freely accelerate for 0.5 seconds before the drag takes effect) - dragWait-=deltaTime; - if (dragWait <= 0f) + else { - dragWait = 0.5f; - - float speed = velocity.Length(); - - dragVec = (velocity / speed) * Math.Min(speed * speed * dragCoefficient * deltaTime, 1.0f); + speed -= drag; + velocity *= speed; } - velocity -= dragVec; } + private void OnWallCollisionInside(Hull prevHull, Vector2 collisionNormal) { if (prevHull == null) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs index 4808fa93c..f54793630 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs @@ -51,25 +51,31 @@ namespace Barotrauma.Particles [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = MaxValue, MinValueFloat = MinValue), Serialize(0f, true)] public float VelocityMax { get; set; } - [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = MaxValue, MinValueFloat = MinValue), Serialize(1f, true)] + [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 100.0f, MinValueFloat = 0.0f), Serialize(1f, true)] public float ScaleMin { get; set; } - [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = MaxValue, MinValueFloat = MinValue), Serialize(1f, true)] + [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 100.0f, MinValueFloat = 0.0f), Serialize(1f, true)] public float ScaleMax { get; set; } [Editable(), Serialize("1,1", true)] public Vector2 ScaleMultiplier { get; set; } - [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = MaxValue, MinValueFloat = MinValue), Serialize(0f, true)] + [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 100.0f, MinValueFloat = 0.0f), Serialize(0f, true)] public float EmitInterval { get; set; } - [Editable, Serialize(0, true)] + [Editable(ValueStep = 1, MinValueInt = 0, MaxValueInt = 1000), Serialize(0, true, description: "The number of particles to spawn per frame, or every x seconds if EmitInterval is set.")] public int ParticleAmount { get; set; } - [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = MaxValue, MinValueFloat = 0), Serialize(0f, true)] + [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 1000.0f, MinValueFloat = 0.0f), Serialize(0f, true)] public float ParticlesPerSecond { get; set; } + [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 10.0f, MinValueFloat = 0.0f), Serialize(0f, true, description: "If larger than 0, a particle is spawned every x pixels across the ray cast by a hitscan weapon.")] + public float EmitAcrossRayInterval { get; set; } + + [Editable(ValueStep = 1, DecimalCount = 2, MaxValueFloat = 100.0f, MinValueFloat = 0.0f), Serialize(0f, true, description: "Delay before the emitter becomes active after being created.")] + public float InitialDelay { get; set; } + [Editable, Serialize(false, true)] public bool HighQualityCollisionDetection { get; set; } @@ -115,6 +121,7 @@ namespace Barotrauma.Particles { private float emitTimer; private float burstEmitTimer; + private float initialDelay; public readonly ParticleEmitterPrefab Prefab; @@ -131,9 +138,30 @@ namespace Barotrauma.Particles public void Emit(float deltaTime, Vector2 position, Hull hullGuess = null, float angle = 0.0f, float particleRotation = 0.0f, float velocityMultiplier = 1.0f, float sizeMultiplier = 1.0f, float amountMultiplier = 1.0f, Color? colorMultiplier = null, ParticlePrefab overrideParticle = null, Tuple tracerPoints = null) { + if (initialDelay < Prefab.Properties.InitialDelay) + { + initialDelay += deltaTime; + return; + } + emitTimer += deltaTime * amountMultiplier; burstEmitTimer -= deltaTime; + if (Prefab.Properties.EmitAcrossRayInterval > 0.0f && tracerPoints != null) + { + Vector2 dir = tracerPoints.Item2 - tracerPoints.Item1; + if (dir.LengthSquared() > 0.001f) + { + float dist = dir.Length(); + dir /= dist; + for (float z = 0.0f; z < dist; z += Prefab.Properties.EmitAcrossRayInterval) + { + Vector2 pos = tracerPoints.Item1 + dir * z; + Emit(pos, hullGuess, angle, particleRotation, velocityMultiplier, sizeMultiplier, colorMultiplier, overrideParticle, tracerPoints: null); + } + } + } + if (Prefab.Properties.ParticlesPerSecond > 0) { float emitInterval = 1.0f / Prefab.Properties.ParticlesPerSecond; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs index adfaeeaa9..104789a59 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs @@ -53,6 +53,10 @@ namespace Barotrauma.Particles [Editable(0.0f, float.MaxValue), Serialize(5.0f, false, description: "How many seconds the particle remains alive.")] public float LifeTime { get; private set; } + [Editable(0.0f, float.MaxValue), Serialize(0.0f, false, description: "Will randomize lifetime value between lifetime and lifetimeMin. If left to 0 will use only lifetime value.")] + public float LifeTimeMin { get; private set; } + + [Editable, Serialize(0.0f, false, description: "How long it takes for the particle to appear after spawning it.")] public float StartDelayMin { get; private set; } [Editable, Serialize(0.0f, false, description: "How long it takes for the particle to appear after spawning it.")] @@ -118,10 +122,10 @@ namespace Barotrauma.Particles [Editable, Serialize(false, false, description: "Should the particle face the direction it's moving towards.")] public bool RotateToDirection { get; private set; } - [Editable, Serialize(0.0f, false, description: "Drag applied to the particle when it's moving through air.")] + [Editable(0.0f, float.MaxValue, DecimalCount = 3), Serialize(0.0f, false, description: "Drag applied to the particle when it's moving through air.")] public float Drag { get; private set; } - [Editable, Serialize(0.0f, false, description: "Drag applied to the particle when it's moving through water.")] + [Editable(0.0f, float.MaxValue, DecimalCount = 3), Serialize(0.0f, false, description: "Drag applied to the particle when it's moving through water.")] public float WaterDrag { get; private set; } private Vector2 velocityChange; @@ -193,8 +197,14 @@ namespace Barotrauma.Particles [Editable, Serialize("1.0,1.0,1.0,1.0", false, description: "The initial color of the particle.")] public Color StartColor { get; private set; } + [Editable, Serialize("1.0,1.0,1.0,1.0", false, description: "The initial color of the particle.")] + public Color MiddleColor { get; private set; } + [Editable, Serialize("1.0,1.0,1.0,1.0", false, description: "The color of the particle at the end of its lifetime.")] public Color EndColor { get; private set; } + + [Editable, Serialize(false, false, description: "If true the color will go from StartColor to EndcColor and back to StartColor.")] + public bool UseMiddleColor { get; private set; } [Editable, Serialize(DrawTargetType.Air, false, description: "Should the particle be rendered in air, water or both.")] public DrawTargetType DrawTarget { get; private set; } @@ -289,7 +299,7 @@ namespace Barotrauma.Particles StartRotationMin = element.GetAttributeFloat("startrotation", 0.0f); StartRotationMax = StartRotationMin; } - + if (CollisionRadius <= 0.0f) CollisionRadius = Sprites.Count > 0 ? 1 : Sprites[0].SourceRect.Width / 2.0f; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs index 945fc9afc..2f6ad173d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs @@ -21,6 +21,7 @@ namespace Barotrauma private GUIComponent locationInfoPanel; private GUIListBox missionList; + private readonly List missionTickBoxes = new List(); private GUIButton repairHullsButton, replaceShuttlesButton, repairItemsButton; @@ -320,6 +321,10 @@ namespace Barotrauma map.SelectLocation(-1); } map.Update(deltaTime, mapContainer); + foreach (GUITickBox tickBox in missionTickBoxes) + { + tickBox.Enabled = Campaign.AllowedToManageCampaign(); + } } public void Update(float deltaTime) @@ -350,6 +355,7 @@ namespace Barotrauma public void SelectLocation(Location location, LocationConnection connection) { + missionTickBoxes.Clear(); locationInfoPanel.ClearChildren(); //don't select the map panel if we're looking at some other tab if (selectedTab == CampaignMode.InteractionType.Map) @@ -451,7 +457,10 @@ namespace Barotrauma if (GUI.MouseOn == tickBox) { return false; } if (tickBox != null) { - tickBox.Selected = !tickBox.Selected; + if (Campaign.AllowedToManageCampaign() && tickBox.Enabled) + { + tickBox.Selected = !tickBox.Selected; + } } return true; }; @@ -489,26 +498,28 @@ namespace Barotrauma }; tickBox.RectTransform.MinSize = new Point(tickBox.Rect.Height, 0); tickBox.RectTransform.IsFixedSize = true; - if (Campaign.AllowedToManageCampaign()) + tickBox.Box.DisabledColor = tickBox.Box.Color * 0.8f; + tickBox.Enabled = Campaign.AllowedToManageCampaign(); + tickBox.OnSelected += (GUITickBox tb) => { - tickBox.OnSelected += (GUITickBox tb) => + if (!Campaign.AllowedToManageCampaign()) { return false; } + + if (tb.Selected) { - if (tb.Selected) - { - Campaign.Map.CurrentLocation.SelectMission(mission); - } - else - { - Campaign.Map.CurrentLocation.DeselectMission(mission); - } - if ((Campaign is MultiPlayerCampaign multiPlayerCampaign) && !multiPlayerCampaign.SuppressStateSending && - Campaign.AllowedToManageCampaign()) - { - GameMain.Client?.SendCampaignState(); - } - return true; - }; - } + Campaign.Map.CurrentLocation.SelectMission(mission); + } + else + { + Campaign.Map.CurrentLocation.DeselectMission(mission); + } + if ((Campaign is MultiPlayerCampaign multiPlayerCampaign) && !multiPlayerCampaign.SuppressStateSending && + Campaign.AllowedToManageCampaign()) + { + GameMain.Client?.SendCampaignState(); + } + return true; + }; + missionTickBoxes.Add(tickBox); GUILayoutGroup difficultyIndicatorGroup = null; if (mission.Difficulty.HasValue) @@ -578,6 +589,8 @@ namespace Barotrauma if (prevSelectedLocation == selectedLocation) { missionList.BarScroll = prevMissionListScroll; + missionList.UpdateDimensions(); + missionList.UpdateScrollBarSize(); } } @@ -615,6 +628,7 @@ namespace Barotrauma StartButton.Visible = false; missionList.Enabled = false; } + //locationInfoPanel?.UpdateAuto(1.0f); } public void SelectTab(CampaignMode.InteractionType tab) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index 0a990cf75..ddb594b69 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -867,9 +867,7 @@ namespace Barotrauma { int.TryParse(maxPlayersBox.Text, out int currMaxPlayers); currMaxPlayers = (int)MathHelper.Clamp(currMaxPlayers + (int)button.UserData, 1, NetConfig.MaxPlayers); - maxPlayersBox.Text = currMaxPlayers.ToString(); - return true; } @@ -1322,8 +1320,18 @@ namespace Barotrauma }; maxPlayersBox = new GUITextBox(new RectTransform(new Vector2(0.6f, 1.0f), buttonContainer.RectTransform), textAlignment: Alignment.Center) { - Text = maxPlayers.ToString(), - CanBeFocused = false + Text = maxPlayers.ToString() + }; + maxPlayersBox.OnEnterPressed += (GUITextBox sender, string text) => + { + maxPlayersBox.Deselect(); + return true; + }; + maxPlayersBox.OnDeselected += (GUITextBox sender, Microsoft.Xna.Framework.Input.Keys key) => + { + int.TryParse(maxPlayersBox.Text, out int currMaxPlayers); + currMaxPlayers = (int)MathHelper.Clamp(currMaxPlayers, 1, NetConfig.MaxPlayers); + maxPlayersBox.Text = currMaxPlayers.ToString(); }; new GUIButton(new RectTransform(new Vector2(0.2f, 1.0f), buttonContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIPlusButton", textAlignment: Alignment.Center) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs index 9b778246a..728752cca 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs @@ -235,7 +235,7 @@ namespace Barotrauma element.Add(new XAttribute("particle", prefab.Identifier)); } - SerializableProperty.SerializeProperties(emitterProperties, element, saveIfDefault: false); + SerializableProperty.SerializeProperties(emitterProperties, element, saveIfDefault: false, ignoreEditable: true); StringBuilder sb = new StringBuilder(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 8f83974cd..3d91c3542 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -2373,10 +2373,10 @@ namespace Barotrauma new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, saveFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); - var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.25f, 0.3f), saveFrame.RectTransform, Anchor.Center) { MinSize = new Point(400, 300) }); + var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.25f, 0.35f), saveFrame.RectTransform, Anchor.Center) { MinSize = new Point(400, 350) }); GUILayoutGroup paddedSaveFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), innerFrame.RectTransform, Anchor.Center)) { - AbsoluteSpacing = 5, + AbsoluteSpacing = GUI.IntScale(5), Stretch = true }; @@ -2393,15 +2393,22 @@ namespace Barotrauma }; #endif - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedSaveFrame.RectTransform), - TextManager.Get("SaveItemAssemblyDialogDescription")); - descriptionBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.3f), paddedSaveFrame.RectTransform)) + var descriptionContainer = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.5f), paddedSaveFrame.RectTransform)); + descriptionBox = new GUITextBox(new RectTransform(Vector2.One, descriptionContainer.Content.RectTransform, Anchor.TopLeft), + font: GUI.SmallFont, style: "GUITextBoxNoBorder", wrap: true, textAlignment: Alignment.TopLeft) { - UserData = "description", - Wrap = true, - Text = "" + Padding = new Vector4(10 * GUI.Scale) }; - + + descriptionBox.OnTextChanged += (textBox, text) => + { + Vector2 textSize = textBox.Font.MeasureString(descriptionBox.WrappedText); + textBox.RectTransform.NonScaledSize = new Point(textBox.RectTransform.NonScaledSize.X, Math.Max(descriptionContainer.Content.Rect.Height, (int)textSize.Y + 10)); + descriptionContainer.UpdateScrollBarSize(); + descriptionContainer.BarScroll = 1.0f; + return true; + }; + var buttonArea = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), paddedSaveFrame.RectTransform), style: null); new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), buttonArea.RectTransform, Anchor.BottomLeft), TextManager.Get("Cancel")) @@ -2417,6 +2424,7 @@ namespace Barotrauma { OnClicked = SaveAssembly }; + buttonArea.RectTransform.MinSize = new Point(0, buttonArea.Children.First().RectTransform.MinSize.Y); } /// @@ -2460,7 +2468,7 @@ namespace Barotrauma } } - bool hideInMenus = !(nameBox.Parent.GetChildByUserData("hideinmenus") is GUITickBox hideInMenusTickBox) ? false : hideInMenusTickBox.Selected; + bool hideInMenus = nameBox.Parent.GetChildByUserData("hideinmenus") is GUITickBox hideInMenusTickBox && hideInMenusTickBox.Selected; #if DEBUG string saveFolder = ItemAssemblyPrefab.VanillaSaveFolder; #else @@ -2479,7 +2487,6 @@ namespace Barotrauma } #endif string filePath = Path.Combine(saveFolder, nameBox.Text + ".xml"); - if (File.Exists(filePath)) { var msgBox = new GUIMessageBox(TextManager.Get("Warning"), TextManager.Get("ItemAssemblyFileExistsWarning"), new[] { TextManager.Get("Yes"), TextManager.Get("No") }); @@ -2494,18 +2501,27 @@ namespace Barotrauma } else { - Save(); + var identifier = nameBox.Text.ToLowerInvariant().Replace(" ", ""); + var existingPrefab = MapEntityPrefab.Find(null, identifier, showErrorMessages: false); + if (existingPrefab != null && System.IO.Path.GetDirectoryName(existingPrefab.FilePath) == ItemAssemblyPrefab.VanillaSaveFolder) + { + var msgBox = new GUIMessageBox(TextManager.Get("Warning"), TextManager.Get("ItemAssemblyVanillaFileExistsWarning")); + } + else + { + Save(); + } } void Save() { - XDocument doc = new XDocument(ItemAssemblyPrefab.Save(MapEntity.SelectedList, nameBox.Text, descriptionBox.Text, hideInMenus)); + XDocument doc = new XDocument(ItemAssemblyPrefab.Save(MapEntity.SelectedList.ToList(), nameBox.Text, descriptionBox.Text, hideInMenus)); #if DEBUG doc.Save(filePath); #else doc.SaveSafe(filePath); #endif - new ItemAssemblyPrefab(filePath); + new ItemAssemblyPrefab(filePath, allowOverwrite: true); UpdateEntityList(); } @@ -3002,8 +3018,11 @@ namespace Barotrauma new ContextMenuOption("SubEditor.PasteAssembly", isEnabled: true, () => PasteAssembly()), new ContextMenuOption("Editor.SelectSame", isEnabled: targets.Count > 0, onSelected: delegate { - IEnumerable matching = MapEntity.mapEntityList.Where(e => e.prefab != null && targets.Any(t => t.prefab?.Identifier == e.prefab.Identifier) && !MapEntity.SelectedList.Contains(e)); - MapEntity.SelectedList.AddRange(matching); + foreach (MapEntity match in MapEntity.mapEntityList.Where(e => e.prefab != null && targets.Any(t => t.prefab?.Identifier == e.prefab.Identifier) && !MapEntity.SelectedList.Contains(e))) + { + if (MapEntity.SelectedList.Contains(match)) { continue; } + MapEntity.SelectedList.Add(match); + } }), new ContextMenuOption("SubEditor.AddImage", isEnabled: true, onSelected: ImageManager.CreateImageWizard), new ContextMenuOption("SubEditor.ToggleImageEditing", isEnabled: true, onSelected: delegate @@ -4716,9 +4735,9 @@ namespace Barotrauma CloseItem(); } } - else if (MapEntity.SelectedList.Count == 1 && WiringMode) + else if (MapEntity.SelectedList.Count == 1 && WiringMode && MapEntity.SelectedList.FirstOrDefault() is Item item) { - (MapEntity.SelectedList[0] as Item)?.UpdateHUD(cam, dummyCharacter, (float)deltaTime); + item.UpdateHUD(cam, dummyCharacter, (float)deltaTime); } CharacterHUD.Update((float)deltaTime, dummyCharacter, cam); diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 5ef085ced..694e942d6 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.1400.1.0 + 0.1400.2.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index edc13608f..6d0f6cba3 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.1400.1.0 + 0.1400.2.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 3829e5364..7d94e6408 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.1400.1.0 + 0.1400.2.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 0fe198237..7b62977ed 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.1400.1.0 + 0.1400.2.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 53c5c401f..d6b119050 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.1400.1.0 + 0.1400.2.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index 151e71450..bcd25ddf9 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -283,7 +283,7 @@ namespace Barotrauma if (extraData != null) { - int min = 0, max = 9; + const int min = 0, max = 9; switch ((NetEntityEvent.Type)extraData[0]) { case NetEntityEvent.Type.InventoryState: @@ -331,6 +331,7 @@ namespace Barotrauma case NetEntityEvent.Type.AssignCampaignInteraction: msg.WriteRangedInteger(6, min, max); msg.Write((byte)CampaignInteractionType); + msg.Write(RequireConsciousnessForCustomInteract); break; case NetEntityEvent.Type.ObjectiveManagerState: msg.WriteRangedInteger(7, min, max); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/EscortMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/EscortMission.cs index 8720caf11..5cb7f95bb 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/EscortMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/EscortMission.cs @@ -19,12 +19,10 @@ namespace Barotrauma { character.WriteSpawnData(msg, character.ID, restrictMessageSize: false); msg.Write(terroristCharacters.Contains(character)); - List characterItems = characterDictionary[character]; - // items must be written in a specific sequence so that child items aren't written before their parents - msg.Write((ushort)characterItems.Count()); - foreach (Item item in characterItems) + msg.Write((ushort)characterItems[character].Count()); + foreach (Item item in characterItems[character]) { - item.WriteSpawnData(msg, item.ID, item.ParentInventory.Owner?.ID ?? Entity.NullEntityID, 0); + item.WriteSpawnData(msg, item.ID, item.ParentInventory?.Owner?.ID ?? Entity.NullEntityID, 0); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/PirateMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/PirateMission.cs index 543caf655..52b5687be 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/PirateMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/PirateMission.cs @@ -19,12 +19,10 @@ namespace Barotrauma foreach (Character character in characters) { character.WriteSpawnData(msg, character.ID, restrictMessageSize: false); - List characterItems = characterDictionary[character]; - // items must be written in a specific sequence so that child items aren't written before their parents - msg.Write((ushort)characterItems.Count()); - foreach (Item item in characterItems) + msg.Write((ushort)characterItems[character].Count()); + foreach (Item item in characterItems[character]) { - item.WriteSpawnData(msg, item.ID, item.ParentInventory.Owner?.ID ?? Entity.NullEntityID, 0); + item.WriteSpawnData(msg, item.ID, item.ParentInventory?.Owner?.ID ?? Entity.NullEntityID, 0); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index 3c6b7c154..0769792c7 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -260,9 +260,13 @@ namespace Barotrauma { Submarine.MainSub = leavingSub; GameMain.GameSession.Submarine = leavingSub; + GameMain.GameSession.SubmarineInfo = leavingSub.Info; + leavingSub.Info.FilePath = System.IO.Path.Combine(SaveUtil.TempPath, leavingSub.Info.Name + ".sub"); var subsToLeaveBehind = GetSubsToLeaveBehind(leavingSub); + GameMain.GameSession.OwnedSubmarines.Add(leavingSub.Info); foreach (Submarine sub in subsToLeaveBehind) { + GameMain.GameSession.OwnedSubmarines.RemoveAll(s => s != leavingSub.Info && s.Name == sub.Info.Name); MapEntity.mapEntityList.RemoveAll(e => e.Submarine == sub && e is LinkedSubmarine); LinkedSubmarine.CreateDummy(leavingSub, sub); } diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 2e5f61e9e..e75eb4474 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.1400.1.0 + 0.1400.2.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml index d95ba89e2..19f21b7a6 100644 --- a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml +++ b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml @@ -109,6 +109,7 @@ + @@ -148,7 +149,7 @@ - + diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index a449ae415..7aba7c7e4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -16,6 +16,13 @@ namespace Barotrauma public enum CirclePhase { Start, CloseIn, FallBack, Advance, Strike } + public enum WallTargetingMethod + { + Target = 0x1, + Heading = 0x2, + Steering = 0x4 + } + partial class EnemyAIController : AIController { public static bool DisableEnemyAI; @@ -54,7 +61,7 @@ namespace Barotrauma private float attackLimbResetTimer; private bool IsAttackRunning => AttackingLimb != null && AttackingLimb.attack.IsRunning; - private bool IsCoolDownRunning => AttackingLimb != null && AttackingLimb.attack.CoolDownTimer > 0; + private bool IsCoolDownRunning => AttackingLimb != null && AttackingLimb.attack.CoolDownTimer > 0 || _previousAttackingLimb != null && _previousAttackingLimb.attack.CoolDownTimer > 0; public float CombatStrength => AIParams.CombatStrength; private float Sight => AIParams.Sight; private float Hearing => AIParams.Hearing; @@ -63,14 +70,18 @@ namespace Barotrauma private FishAnimController FishAnimController => Character.AnimController as FishAnimController; - //the limb selected for the current attack private Limb _attackingLimb; + private Limb _previousAttackingLimb; public Limb AttackingLimb { get { return _attackingLimb; } private set { attackLimbResetTimer = 0; + if (_attackingLimb != value) + { + _previousAttackingLimb = _attackingLimb; + } _attackingLimb = value; attackVector = null; Reverse = _attackingLimb != null && _attackingLimb.attack.Reverse; @@ -342,6 +353,10 @@ namespace Barotrauma { targetingTag = "weaker"; } + else + { + targetingTag = "equal"; + } } } } @@ -480,10 +495,6 @@ namespace Barotrauma { CharacterParams.TargetParams targetingParams = null; UpdateTargets(Character, out targetingParams); - if (!IsLatchedOnSub) - { - UpdateWallTarget(requiredHoleCount); - } updateTargetsTimer = updateTargetsInterval * Rand.Range(0.75f, 1.25f); if (SelectedAiTarget == null) { @@ -494,6 +505,10 @@ namespace Barotrauma selectedTargetingParams = targetingParams; State = targetingParams.State; } + if (SelectedAiTarget?.Entity != null && !IsLatchedOnSub && State == AIState.Attack || State == AIState.Aggressive || State == AIState.PassiveAggressive) + { + UpdateWallTarget(requiredHoleCount); + } } } @@ -1004,12 +1019,8 @@ namespace Barotrauma if (!w.SectionBodyDisabled(i)) { isBroken = false; - Vector2 sectionPos = w.SectionPosition(i); + Vector2 sectionPos = w.SectionPosition(i, world: true); attackWorldPos = sectionPos; - if (w.Submarine != null) - { - attackWorldPos += w.Submarine.Position; - } attackSimPos = ConvertUnits.ToSimUnits(attackWorldPos); break; } @@ -1027,18 +1038,19 @@ namespace Barotrauma bool pursue = false; if (IsCoolDownRunning) { - if (AttackingLimb.attack.CoolDownTimer >= AttackingLimb.attack.CoolDown + AttackingLimb.attack.CurrentRandomCoolDown - AttackingLimb.attack.AfterAttackDelay) + var currentAttackLimb = AttackingLimb ?? _previousAttackingLimb; + if (currentAttackLimb.attack.CoolDownTimer >= currentAttackLimb.attack.CoolDown + currentAttackLimb.attack.CurrentRandomCoolDown - currentAttackLimb.attack.AfterAttackDelay) { return; } - switch (AttackingLimb.attack.AfterAttack) + switch (currentAttackLimb.attack.AfterAttack) { case AIBehaviorAfterAttack.Pursue: case AIBehaviorAfterAttack.PursueIfCanAttack: - if (AttackingLimb.attack.SecondaryCoolDown <= 0) + if (currentAttackLimb.attack.SecondaryCoolDown <= 0) { // No (valid) secondary cooldown defined. - if (AttackingLimb.attack.AfterAttack == AIBehaviorAfterAttack.Pursue) + if (currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.Pursue) { canAttack = false; pursue = true; @@ -1051,13 +1063,13 @@ namespace Barotrauma } else { - if (AttackingLimb.attack.SecondaryCoolDownTimer <= 0) + if (currentAttackLimb.attack.SecondaryCoolDownTimer <= 0) { // Don't allow attacking when the attack target has just changed. if (_previousAiTarget != null && SelectedAiTarget != _previousAiTarget) { canAttack = false; - if (AttackingLimb.attack.AfterAttack == AIBehaviorAfterAttack.PursueIfCanAttack) + if (currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.PursueIfCanAttack) { // Fall back if cannot attack. UpdateFallBack(attackWorldPos, deltaTime, true); @@ -1068,7 +1080,7 @@ namespace Barotrauma else { // If the secondary cooldown is defined and expired, check if we can switch the attack - var newLimb = GetAttackLimb(attackWorldPos, AttackingLimb); + var newLimb = GetAttackLimb(attackWorldPos, currentAttackLimb); if (newLimb != null) { // Attack with the new limb @@ -1077,7 +1089,7 @@ namespace Barotrauma else { // No new limb was found. - if (AttackingLimb.attack.AfterAttack == AIBehaviorAfterAttack.Pursue) + if (currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.Pursue) { canAttack = false; pursue = true; @@ -1099,26 +1111,26 @@ namespace Barotrauma break; case AIBehaviorAfterAttack.FallBackUntilCanAttack: case AIBehaviorAfterAttack.FollowThroughUntilCanAttack: - if (AttackingLimb.attack.SecondaryCoolDown <= 0) + if (currentAttackLimb.attack.SecondaryCoolDown <= 0) { // No (valid) secondary cooldown defined. - UpdateFallBack(attackWorldPos, deltaTime, AttackingLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); + UpdateFallBack(attackWorldPos, deltaTime, currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); return; } else { - if (AttackingLimb.attack.SecondaryCoolDownTimer <= 0) + if (currentAttackLimb.attack.SecondaryCoolDownTimer <= 0) { // Don't allow attacking when the attack target has just changed. if (_previousAiTarget != null && SelectedAiTarget != _previousAiTarget) { - UpdateFallBack(attackWorldPos, deltaTime, AttackingLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); + UpdateFallBack(attackWorldPos, deltaTime, currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); return; } else { // If the secondary cooldown is defined and expired, check if we can switch the attack - var newLimb = GetAttackLimb(attackWorldPos, AttackingLimb); + var newLimb = GetAttackLimb(attackWorldPos, currentAttackLimb); if (newLimb != null) { // Attack with the new limb @@ -1127,7 +1139,7 @@ namespace Barotrauma else { // No new limb was found. - UpdateFallBack(attackWorldPos, deltaTime, AttackingLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); + UpdateFallBack(attackWorldPos, deltaTime, currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); return; } } @@ -1135,13 +1147,13 @@ namespace Barotrauma else { // Cooldown not yet expired -> steer away from the target - UpdateFallBack(attackWorldPos, deltaTime, AttackingLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); + UpdateFallBack(attackWorldPos, deltaTime, currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack); return; } } break; case AIBehaviorAfterAttack.IdleUntilCanAttack: - if (AttackingLimb.attack.SecondaryCoolDown <= 0) + if (currentAttackLimb.attack.SecondaryCoolDown <= 0) { // No (valid) secondary cooldown defined. UpdateIdle(deltaTime, followLastTarget: false); @@ -1149,7 +1161,7 @@ namespace Barotrauma } else { - if (AttackingLimb.attack.SecondaryCoolDownTimer <= 0) + if (currentAttackLimb.attack.SecondaryCoolDownTimer <= 0) { // Don't allow attacking when the attack target has just changed. if (_previousAiTarget != null && SelectedAiTarget != _previousAiTarget) @@ -1160,7 +1172,7 @@ namespace Barotrauma else { // If the secondary cooldown is defined and expired, check if we can switch the attack - var newLimb = GetAttackLimb(attackWorldPos, AttackingLimb); + var newLimb = GetAttackLimb(attackWorldPos, currentAttackLimb); if (newLimb != null) { // Attack with the new limb @@ -1258,7 +1270,7 @@ namespace Barotrauma Vector2 attackLimbPos = Character.AnimController.SimplePhysicsEnabled ? Character.WorldPosition : AttackingLimb.WorldPosition; Vector2 toTarget = attackWorldPos - attackLimbPos; - // Add a margin when the target is moving away, because otherwise it might be difficult to reach it (the attack takes some time to perform) + // Add a margin when the target is moving away, because otherwise it might be difficult to reach it if the attack takes some time to execute if (wallTarget != null) { if (wallTarget.Structure.Submarine != null) @@ -1283,9 +1295,14 @@ namespace Barotrauma Vector2 CalculateMargin(Vector2 targetVelocity) { - if (targetVelocity == Vector2.Zero) { return targetVelocity; } + if (targetVelocity == Vector2.Zero) { return Vector2.Zero; } + float diff = AttackingLimb.attack.Range - AttackingLimb.attack.DamageRange; + if (diff <= 0 || toTarget.LengthSquared() <= MathUtils.Pow2(AttackingLimb.attack.DamageRange)) { return Vector2.Zero; } float dot = Vector2.Dot(Vector2.Normalize(targetVelocity), Vector2.Normalize(Character.AnimController.Collider.LinearVelocity)); - return ConvertUnits.ToDisplayUnits(targetVelocity) * AttackingLimb.attack.Duration * dot; + if (dot <= 0 || !MathUtils.IsValid(dot)) { return Vector2.Zero; } + float distanceOffset = diff * AttackingLimb.attack.Duration; + // Intentionally omit the unit conversion because we use distanceOffset as a multiplier. + return targetVelocity * distanceOffset * dot; } // Check that we can reach the target @@ -1649,7 +1666,7 @@ namespace Barotrauma float GetTargetMaxSpeed() => Character.ApplyTemporarySpeedLimits(Character.AnimController.CurrentSwimParams.MovementSpeed * 0.3f); } SteeringManager.SteeringSeek(steerPos, 10); - if (SelectedAiTarget?.Entity is Character || distance == 0 || distance > ConvertUnits.ToDisplayUnits(avoidLookAheadDistance * 2)) + if (SelectedAiTarget?.Entity is Character c && c.Submarine == null || distance == 0 || distance > ConvertUnits.ToDisplayUnits(avoidLookAheadDistance * 2)) { SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 30); } @@ -1813,9 +1830,8 @@ namespace Barotrauma ChangeTargetState(attacker, canAttack ? AIState.Attack : AIState.Escape, 100); } } - else + else if (!AIParams.HasTag("equal")) { - // Equal strength ChangeTargetState(attacker, canAttack ? AIState.Attack : AIState.Escape, 100); } } @@ -2152,6 +2168,10 @@ namespace Barotrauma { targetingTag = "weaker"; } + else + { + targetingTag = "equal"; + } if (targetingTag == "stronger" && (State == AIState.Avoid || State == AIState.Escape || State == AIState.Flee)) { if (SelectedAiTarget == aiTarget) @@ -2619,91 +2639,163 @@ namespace Barotrauma } private WallTarget wallTarget; - + private readonly List<(Body, int, Vector2)> wallHits = new List<(Body, int, Vector2)>(3); private void UpdateWallTarget(int requiredHoleCount) { wallTarget = null; - if (State == AIState.Flee || State == AIState.Escape) { return; } if (AIParams.CanOpenDoors && HasValidPath(requireNonDirty: true)) { return; } if (SelectedAiTarget == null) { return; } - if (SelectedAiTarget.Entity == null) { return; } - Vector2 rayStart = SimPosition; - Vector2 rayEnd = SelectedAiTarget.SimPosition; - if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null) + if (SelectedAiTarget.Entity == null) { return; } + wallHits.Clear(); + Structure wall = null; + if (AIParams.WallTargetingMethod.HasFlag(WallTargetingMethod.Target)) { - rayStart -= SelectedAiTarget.Entity.Submarine.SimPosition; - } - else if (SelectedAiTarget.Entity.Submarine == null && Character.Submarine != null) - { - rayEnd -= Character.Submarine.SimPosition; - } - Body closestBody = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true, ignoreSensors: CanEnterSubmarine, ignoreDisabledWalls: CanEnterSubmarine); - if (Submarine.LastPickedFraction != 1.0f && closestBody != null) - { - if (closestBody.UserData is Structure wall && wall.Submarine != null && (Character.IsBot || wall.Submarine.Info.IsPlayer || wall.Submarine.Info.IsOutpost && TargetOutposts)) + Vector2 rayStart = SimPosition; + Vector2 rayEnd = SelectedAiTarget.SimPosition; + if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null) { - int sectionIndex = wall.FindSectionIndex(ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition)); - float sectionDamage = wall.SectionDamage(sectionIndex); - for (int i = sectionIndex - 2; i <= sectionIndex + 2; i++) + rayStart -= SelectedAiTarget.Entity.Submarine.SimPosition; + } + else if (SelectedAiTarget.Entity.Submarine == null && Character.Submarine != null) + { + rayEnd -= Character.Submarine.SimPosition; + } + DoRayCast(rayStart, rayEnd); + } + if (AIParams.WallTargetingMethod.HasFlag(WallTargetingMethod.Heading)) + { + Vector2 rayStart = SimPosition; + Vector2 rayEnd = rayStart + VectorExtensions.Forward(Character.AnimController.Collider.Rotation + MathHelper.PiOver2, avoidLookAheadDistance * 5); + if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null) + { + rayStart -= SelectedAiTarget.Entity.Submarine.SimPosition; + rayEnd -= SelectedAiTarget.Entity.Submarine.SimPosition; + } + else if (SelectedAiTarget.Entity.Submarine == null && Character.Submarine != null) + { + rayStart -= Character.Submarine.SimPosition; + rayEnd -= Character.Submarine.SimPosition; + } + DoRayCast(rayStart, rayEnd); + } + if (AIParams.WallTargetingMethod.HasFlag(WallTargetingMethod.Steering)) + { + Vector2 rayStart = SimPosition; + Vector2 rayEnd = rayStart + Steering * 5; + if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null) + { + rayStart -= SelectedAiTarget.Entity.Submarine.SimPosition; + rayEnd -= SelectedAiTarget.Entity.Submarine.SimPosition; + } + else if (SelectedAiTarget.Entity.Submarine == null && Character.Submarine != null) + { + rayStart -= Character.Submarine.SimPosition; + rayEnd -= Character.Submarine.SimPosition; + } + DoRayCast(rayStart, rayEnd); + } + if (wallHits.Any()) + { + Body closestBody = null; + float closestDistance = 0; + int sectionIndex = -1; + Vector2 sectionPos = Vector2.Zero; + foreach ((Body body, int index, Vector2 sectionPosition) in wallHits) + { + float distance = Vector2.DistanceSquared(SimPosition, sectionPosition); + if (closestBody == null || closestDistance == 0 || distance < closestDistance) { - if (wall.SectionBodyDisabled(i)) - { - if (Character.AnimController.CanEnterSubmarine && CanPassThroughHole(wall, i, requiredHoleCount)) - { - sectionIndex = i; - break; - } - else - { - // Ignore and keep breaking other sections - continue; - } - } - if (wall.SectionDamage(i) > sectionDamage) - { - sectionIndex = i; - } - } - Vector2 sectionPos = wall.SectionPosition(sectionIndex); - Vector2 attachTargetNormal; - if (wall.IsHorizontal) - { - attachTargetNormal = new Vector2(0.0f, Math.Sign(WorldPosition.Y - wall.WorldPosition.Y)); - sectionPos.Y += (wall.BodyHeight <= 0.0f ? wall.Rect.Height : wall.BodyHeight) / 2 * attachTargetNormal.Y; - } - else - { - attachTargetNormal = new Vector2(Math.Sign(WorldPosition.X - wall.WorldPosition.X), 0.0f); - sectionPos.X += (wall.BodyWidth <= 0.0f ? wall.Rect.Width : wall.BodyWidth) / 2 * attachTargetNormal.X; - } - LatchOntoAI?.SetAttachTarget(wall, ConvertUnits.ToSimUnits(sectionPos), attachTargetNormal); - if (Character.AnimController.CanEnterSubmarine || !wall.SectionBodyDisabled(sectionIndex) && !IsWallDisabled(wall)) - { - if (AIParams.TargetOuterWalls || wall.prefab.Tags.Contains("inner") || wall.Submarine != null && wall.Submarine == Character.Submarine) - { - if (wall.NoAITarget && Character.AnimController.CanEnterSubmarine) - { - // Blocked by a wall that shouldn't be targeted. The main intention here is to prevents monsters from entering the the tail and the nose pieces. - IgnoreTarget(SelectedAiTarget); - ResetAITarget(); - } - else - { - wallTarget = new WallTarget(sectionPos, wall, sectionIndex); - } - } + closestBody = body; + closestDistance = distance; + wall = closestBody.UserData as Structure; + sectionPos = sectionPosition; + sectionIndex = index; } } - if (!Character.AnimController.CanEnterSubmarine && wallTarget == null && selectedTargetingParams?.AttackPattern == AttackPattern.Straight) + if (closestBody == null || sectionIndex == -1) { return; } + Vector2 attachTargetNormal; + if (wall.IsHorizontal) { - if (closestBody.UserData is Structure w && w.Submarine != null && w.Submarine == SelectedAiTarget.Entity?.Submarine || - closestBody.UserData is Item i && i.Submarine != null && i.Submarine == SelectedAiTarget.Entity?.Submarine) + attachTargetNormal = new Vector2(0.0f, Math.Sign(WorldPosition.Y - wall.WorldPosition.Y)); + sectionPos.Y += (wall.BodyHeight <= 0.0f ? wall.Rect.Height : wall.BodyHeight) / 2 * attachTargetNormal.Y; + } + else + { + attachTargetNormal = new Vector2(Math.Sign(WorldPosition.X - wall.WorldPosition.X), 0.0f); + sectionPos.X += (wall.BodyWidth <= 0.0f ? wall.Rect.Width : wall.BodyWidth) / 2 * attachTargetNormal.X; + } + LatchOntoAI?.SetAttachTarget(wall, ConvertUnits.ToSimUnits(sectionPos), attachTargetNormal); + if (Character.AnimController.CanEnterSubmarine || !wall.SectionBodyDisabled(sectionIndex) && !IsWallDisabled(wall)) + { + if (wall.NoAITarget && Character.AnimController.CanEnterSubmarine) { - // Cannot reach the target, because it's blocked by a disabled wall or a door + // Blocked by a wall that shouldn't be targeted. The main intention here is to prevent monsters from entering the the tail and the nose pieces. IgnoreTarget(SelectedAiTarget); ResetAITarget(); } + else + { + wallTarget = new WallTarget(sectionPos, wall, sectionIndex); + } } + else + { + // Blocked by a disabled wall. + IgnoreTarget(SelectedAiTarget); + ResetAITarget(); + } + } + + void DoRayCast(Vector2 rayStart, Vector2 rayEnd) + { + Body hitTarget = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true, ignoreSensors: CanEnterSubmarine, ignoreDisabledWalls: CanEnterSubmarine); + if (hitTarget != null && IsValid(hitTarget, out wall)) + { + int sectionIndex = wall.FindSectionIndex(ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition)); + if (sectionIndex >= 0) + { + wallHits.Add((hitTarget, sectionIndex, GetSectionPosition(wall, sectionIndex))); + } + } + } + + Vector2 GetSectionPosition(Structure wall, int sectionIndex) + { + float sectionDamage = wall.SectionDamage(sectionIndex); + for (int i = sectionIndex - 2; i <= sectionIndex + 2; i++) + { + if (wall.SectionBodyDisabled(i)) + { + if (Character.AnimController.CanEnterSubmarine && CanPassThroughHole(wall, i, requiredHoleCount)) + { + sectionIndex = i; + break; + } + else + { + // Ignore and keep breaking other sections + continue; + } + } + if (wall.SectionDamage(i) > sectionDamage) + { + sectionIndex = i; + } + } + return wall.SectionPosition(sectionIndex, world: false); + } + + bool IsValid(Body hit, out Structure wall) + { + wall = null; + if (Submarine.LastPickedFraction == 1.0f) { return false; } + if (!(hit.UserData is Structure w)) { return false; } + if (w.Submarine == null) { return false; } + if (w.Submarine != SelectedAiTarget.Entity.Submarine) { return false; } + if (Character.Submarine == null && w.prefab.Tags.Contains("inner")) { return false; } + if (!AIParams.TargetOuterWalls && !w.prefab.Tags.Contains("inner")) { return false; } + wall = w; + return true; } } @@ -2712,7 +2804,7 @@ namespace Barotrauma if (wallTarget != null && wallTarget.SectionIndex > -1 && CanPassThroughHole(wallTarget.Structure, wallTarget.SectionIndex, requiredHoleCount)) { WallSection section = wallTarget.Structure.GetSection(wallTarget.SectionIndex); - Vector2 targetPos = wallTarget.Structure.SectionPosition(wallTarget.SectionIndex, true); + Vector2 targetPos = wallTarget.Structure.SectionPosition(wallTarget.SectionIndex, world: true); return section?.gap != null && SteerThroughGap(wallTarget.Structure, section, targetPos, deltaTime); } else if (SelectedAiTarget != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 680b2ff77..5a1a04001 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -136,8 +136,9 @@ namespace Barotrauma } public override bool IsMentallyUnstable => - MentalStateManager?.CurrentMentalType != MentalStateManager.MentalType.Normal && - MentalStateManager?.CurrentMentalType != MentalStateManager.MentalType.Confused; + MentalStateManager == null ? false : + MentalStateManager.CurrentMentalType != MentalStateManager.MentalType.Normal && + MentalStateManager.CurrentMentalType != MentalStateManager.MentalType.Confused; public ShipCommandManager ShipCommandManager { get; private set; } @@ -740,9 +741,10 @@ namespace Barotrauma suitableContainer = null; if (character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: ignoredItems, positionalReference: containableItem, customPriorityFunction: i => { - if (i.IsThisOrAnyContainerIgnoredByAI()) { return 0; } + if (i.IsThisOrAnyContainerIgnoredByAI(character)) { return 0; } var container = i.GetComponent(); if (container == null) { return 0; } + if (!container.HasAccess(character)) { return 0; } if (!container.Inventory.CanBePut(containableItem)) { return 0; } if (container.ShouldBeContained(containableItem, out bool isRestrictionsDefined)) { @@ -1032,11 +1034,11 @@ namespace Barotrauma return; } float cumulativeDamage = GetDamageDoneByAttacker(attacker); - if (!Character.IsSecurity && attacker.IsBot && !IsMentallyUnstable && !attacker.AIController.IsMentallyUnstable && Character.CombatAction == null) + bool isAccidental = attacker.IsBot && !IsMentallyUnstable && !attacker.AIController.IsMentallyUnstable && Character.CombatAction == null; + if (isAccidental) { - if (cumulativeDamage > 1) + if (!Character.IsSecurity && cumulativeDamage > 1) { - // Don't retaliate on damage done by friendly NPC, because we know it's accidental, unless if it's a berserking AI AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker); } } @@ -1109,7 +1111,7 @@ namespace Barotrauma { foreach (Character otherCharacter in Character.CharacterList) { - if (otherCharacter == Character || otherCharacter.IsDead || otherCharacter.IsUnconscious || otherCharacter.Removed) { continue; } + if (otherCharacter == Character || otherCharacter.IsUnconscious || otherCharacter.Removed) { continue; } if (otherCharacter.Submarine != Character.Submarine) { continue; } if (otherCharacter.Submarine != attacker.Submarine) { continue; } if (otherCharacter.Info?.Job == null || otherCharacter.IsInstigator) { continue; } @@ -1921,18 +1923,45 @@ namespace Barotrauma private static bool FilterCrewMember(Character self, Character other) => other != null && !other.IsDead && !other.Removed && other.AIController is HumanAIController humanAi && humanAi.IsFriendly(self); - public static bool IsItemOperatedByAnother(Character character, ItemComponent target, out Character operatingCharacter) + public static bool IsItemTargetedBySomeone(ItemComponent target, CharacterTeamType team, out Character operatingCharacter) { operatingCharacter = null; - if (character == null) { return false; } - if (target?.Item == null) { return false; } - bool isOrder = IsOrderedToOperateThis(character.AIController); - foreach (var c in Character.CharacterList) + foreach (Character c in Character.CharacterList) { - if (c == character) { continue; } - if (c.IsDead || c.IsIncapacitated) { continue; } - if (!IsFriendly(character, c, onlySameTeam: true)) { continue; } + if (c.Removed) { continue; } + if (c.TeamID != team) { continue; } + if (c.IsIncapacitated) { continue; } + bool isOperated = c.SelectedConstruction == target.Item; + if (!isOperated) + { + if (c.AIController is HumanAIController humanAI) + { + isOperated = humanAI.ObjectiveManager.Objectives.Any(o => o is AIObjectiveOperateItem operateObjective && operateObjective.Component.Item == target.Item); + } + } operatingCharacter = c; + if (isOperated) + { + return true; + } + } + return false; + } + + // There's some duplicate logic in the two methods below, but making them use the same code would require some changes in the target classes so that we could use exactly the same checks. + // And even then there would be some differences that could end up being confusing (like the exception for steering). + public bool IsItemOperatedByAnother(ItemComponent target, out Character other) + { + other = null; + if (target?.Item == null) { return false; } + bool isOrder = IsOrderedToOperateThis(Character.AIController); + foreach (Character c in Character.CharacterList) + { + if (c == Character) { continue; } + if (c.Removed) { continue; } + if (c.TeamID != Character.TeamID) { continue; } + if (c.IsIncapacitated) { continue; } + other = c; if (c.IsPlayer) { if (c.SelectedConstruction == target.Item) @@ -1963,7 +1992,7 @@ namespace Barotrauma } else { - if (!isTargetOrdered && operatingAI.ObjectiveManager.CurrentOrder == operatingAI.ObjectiveManager.CurrentObjective) + if (!isTargetOrdered && operatingAI.ObjectiveManager.CurrentOrder != operatingAI.ObjectiveManager.CurrentObjective) { // The other bot is ordered to do something else continue; @@ -1971,12 +2000,12 @@ namespace Barotrauma if (target is Steering) { // Steering is hard-coded -> cannot use the required skills collection defined in the xml - if (character.GetSkillLevel("helm") <= c.GetSkillLevel("helm")) + if (Character.GetSkillLevel("helm") <= c.GetSkillLevel("helm")) { return true; } } - else if (target.DegreeOfSuccess(character) <= target.DegreeOfSuccess(c)) + else if (target.DegreeOfSuccess(Character) <= target.DegreeOfSuccess(c)) { return true; } @@ -1985,7 +2014,65 @@ namespace Barotrauma } } return false; - bool IsOrderedToOperateThis(AIController ai) => ai is HumanAIController humanAI && humanAI.ObjectiveManager.CurrentOrder is AIObjectiveOperateItem operateObjective && operateObjective.Component.Item == target.Item; + bool IsOrderedToOperateThis(AIController ai) => ai is HumanAIController humanAI && humanAI.ObjectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder.Component.Item == target.Item; + } + + public bool IsItemRepairedByAnother(Item target, out Character other) + { + other = null; + if (Character == null) { return false; } + if (target == null) { return false; } + bool isOrder = IsOrderedToRepairThis(Character.AIController as HumanAIController); + foreach (var c in Character.CharacterList) + { + if (c == Character) { continue; } + if (c.TeamID != Character.TeamID) { continue; } + if (c.IsIncapacitated) { continue; } + other = c; + if (c.IsPlayer) + { + if (target.Repairables.Any(r => r.CurrentFixer == c)) + { + // If the other character is player, don't try to repair + return true; + } + } + else if (c.AIController is HumanAIController operatingAI) + { + var repairItemsObjective = operatingAI.ObjectiveManager.GetObjective(); + if (repairItemsObjective == null) { continue; } + if (repairItemsObjective.SubObjectives.None(o => o is AIObjectiveRepairItem repairObjective && repairObjective.Item == target)) + { + // Not targeting the same item. + continue; + } + bool isTargetOrdered = IsOrderedToRepairThis(operatingAI); + if (!isOrder && isTargetOrdered) + { + // If the other bot is ordered to repair the item, let him do it, unless we are ordered too + return true; + } + else + { + if (isOrder && !isTargetOrdered) + { + // We are ordered and the target is not -> allow to repair + continue; + } + else + { + if (!isTargetOrdered && operatingAI.ObjectiveManager.CurrentOrder != operatingAI.ObjectiveManager.CurrentObjective) + { + // The other bot is ordered to do something else + continue; + } + return target.Repairables.Max(r => r.DegreeOfSuccess(Character)) <= target.Repairables.Max(r => r.DegreeOfSuccess(c)); + } + } + } + } + return false; + bool IsOrderedToRepairThis(HumanAIController ai) => ai.ObjectiveManager.CurrentOrder is AIObjectiveRepairItems repairOrder && repairOrder.PrioritizedItem == target; } #region Wrappers @@ -1994,7 +2081,6 @@ namespace Barotrauma public bool IsTrueForAnyCrewMember(Func predicate) => IsTrueForAnyCrewMember(Character, predicate); public bool IsTrueForAllCrewMembers(Func predicate) => IsTrueForAllCrewMembers(Character, predicate); public int CountCrew(Func predicate = null, bool onlyActive = true, bool onlyBots = false) => CountCrew(Character, predicate, onlyActive, onlyBots); - public bool IsItemOperatedByAnother(ItemComponent target, out Character operatingCharacter) => IsItemOperatedByAnother(Character, target, out operatingCharacter); #endregion } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs index cc94b8be1..ae6eb6ead 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs @@ -20,7 +20,7 @@ namespace Barotrauma { if (battery == null) { return false; } var item = battery.Item; - if (item.IgnoreByAI) { return false; } + if (item.IgnoreByAI(character)) { return false; } if (!item.IsInteractable(character)) { return false; } if (item.Submarine == null) { return false; } if (item.CurrentHull == null) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs index ec52246bf..e2ca2f781 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs @@ -61,7 +61,7 @@ namespace Barotrauma protected override void Act(float deltaTime) { - if (item.IgnoreByAI) + if (item.IgnoreByAI(character)) { Abandon = true; return; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs index 02c23ffab..f0cf653a8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs @@ -82,16 +82,17 @@ namespace Barotrauma public static bool IsValidContainer(Item container, Character character, bool allowUnloading = true) => allowUnloading && - !container.IgnoreByAI && + !container.IgnoreByAI(character) && container.IsInteractable(character) && container.HasTag("allowcleanup") && container.ParentInventory == null && container.OwnInventory != null && container.OwnInventory.AllItems.Any() && + container.GetComponent() is ItemContainer itemContainer && itemContainer.HasAccess(character) && IsItemInsideValidSubmarine(container, character); public static bool IsValidTarget(Item item, Character character, bool checkInventory, bool allowUnloading = true) { if (item == null) { return false; } - if (item.IgnoreByAI) { return false; } + if (item.IgnoreByAI(character)) { return false; } if (!item.IsInteractable(character)) { return false; } if (item.SpawnedInOutpost) { return false; } if (item.ParentInventory != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs index 763c92de9..b7d66bb08 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs @@ -64,7 +64,7 @@ namespace Barotrauma protected override bool CheckObjectiveSpecific() { if (IsCompleted) { return true; } - if (container == null || (container.Item != null && container.Item.IsThisOrAnyContainerIgnoredByAI())) + if (container == null || (container.Item != null && container.Item.IsThisOrAnyContainerIgnoredByAI(character))) { Abandon = true; return false; @@ -87,11 +87,11 @@ namespace Barotrauma } } - private bool CheckItem(Item i) => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)) && i.ConditionPercentage >= ConditionLevel && !i.IsThisOrAnyContainerIgnoredByAI(); + private bool CheckItem(Item i) => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)) && i.ConditionPercentage >= ConditionLevel && !i.IsThisOrAnyContainerIgnoredByAI(character); protected override void Act(float deltaTime) { - if (container?.Item == null || container.Item.Removed || container.Item.IsThisOrAnyContainerIgnoredByAI()) + if (container?.Item == null || container.Item.Removed || container.Item.IsThisOrAnyContainerIgnoredByAI(character)) { Abandon = true; return; @@ -147,7 +147,7 @@ namespace Barotrauma DialogueIdentifier = "dialogcannotreachtarget", TargetName = container.Item.Name, AbortCondition = obj => - container?.Item == null || container.Item.Removed || container.Item.IsThisOrAnyContainerIgnoredByAI() || + container?.Item == null || container.Item.Removed || container.Item.IsThisOrAnyContainerIgnoredByAI(character) || ItemToContain == null || ItemToContain.Removed || !ItemToContain.IsOwnedBy(character) || container.Item.GetRootInventoryOwner() is Character c && c != character, SpeakIfFails = !objectiveManager.IsCurrentOrder() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs index 5039751ff..559aad746 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs @@ -63,13 +63,16 @@ namespace Barotrauma protected override void Act(float deltaTime) { - Item itemToDecontain = targetItem ?? sourceContainer.Inventory.FindItem(i => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id) && !i.IgnoreByAI), recursive: false); + Item itemToDecontain = + targetItem ?? + sourceContainer.Inventory.FindItem(i => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id) && !i.IgnoreByAI(character)), recursive: false); + if (itemToDecontain == null) { Abandon = true; return; } - if (itemToDecontain.IgnoreByAI) + if (itemToDecontain.IgnoreByAI(character)) { Abandon = true; return; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs index 374c22fa2..87fab661f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs @@ -38,10 +38,13 @@ namespace Barotrauma Priority = 0; Abandon = true; } - else if (HumanAIController.IsTrueForAnyCrewMember(other => other != HumanAIController && other.Character.IsBot && other.ObjectiveManager.GetActiveObjective()?.Leak == Leak)) + else if (HumanAIController.IsTrueForAnyCrewMember( + other => other != HumanAIController && + other.Character.IsBot && + other.ObjectiveManager.GetActiveObjective() is AIObjectiveFixLeaks fixLeaks && + fixLeaks.SubObjectives.Any(so => so is AIObjectiveFixLeak fixObjective && fixObjective.Leak == Leak))) { Priority = 0; - Abandon = true; } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs index 49e2978d0..5f7d91295 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs @@ -74,7 +74,7 @@ namespace Barotrauma // Don't fix a leak on a wall section set to be ignored if (gap.ConnectedWall != null) { - if (gap.ConnectedWall.Sections.Any(s => s.gap == gap && s.IgnoreByAI)) { return false; } + if (gap.ConnectedWall.Sections.Any(s => s.gap == gap && s.IgnoreByAI(character))) { return false; } if (gap.ConnectedWall.MaxHealth <= 0.0f) { return false; } } if (gap.ConnectedWall == null || gap.ConnectedDoor != null || gap.Open <= 0 || gap.linkedTo.All(l => l == null)) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs index dbd640432..870b10333 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs @@ -305,6 +305,7 @@ namespace Barotrauma if (rootInventoryOwner is Item ownerItem) { if (!ownerItem.IsInteractable(character)) { continue; } + if (!(ownerItem.GetComponent()?.HasRequiredItems(character, addMessage: false) ?? true)) { continue; } } Vector2 itemPos = (rootInventoryOwner ?? item).WorldPosition; float yDist = Math.Abs(character.WorldPosition.Y - itemPos.Y); @@ -403,7 +404,7 @@ namespace Barotrauma private bool CheckItem(Item item) { if (!item.IsInteractable(character)) { return false; } - if (item.IsThisOrAnyContainerIgnoredByAI()) { return false; } + if (item.IsThisOrAnyContainerIgnoredByAI(character)) { return false; } if (ignoredItems.Contains(item)) { return false; }; if (item.Condition < TargetCondition) { return false; } if (ItemFilter != null && !ItemFilter(item)) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs index 2ce8cd4c3..5db86b049 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs @@ -118,7 +118,7 @@ namespace Barotrauma targetItem.CurrentHull.FireSources.Any() || HumanAIController.IsItemOperatedByAnother(target, out _) || Character.CharacterList.Any(c => c.CurrentHull == targetItem.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c)) - || component.Item.IgnoreByAI || (useController && controller.Item.IgnoreByAI)) + || component.Item.IgnoreByAI(character) || useController && controller.Item.IgnoreByAI(character)) { Priority = 0; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs index e398263aa..720cbd730 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs @@ -27,7 +27,7 @@ namespace Barotrauma protected override bool Filter(Pump pump) { if (pump == null) { return false; } - if (pump.Item.IgnoreByAI) { return false; } + if (pump.Item.IgnoreByAI(character)) { return false; } if (!pump.Item.IsInteractable(character)) { return false; } if (pump.Item.HasTag("ballast")) { return false; } if (pump.Item.Submarine == null) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs index 70699139c..8e1c022ce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs @@ -34,7 +34,7 @@ namespace Barotrauma protected override float GetPriority() { - if (!IsAllowed || Item.IgnoreByAI) + if (!IsAllowed || Item.IgnoreByAI(character)) { Priority = 0; Abandon = true; @@ -44,10 +44,10 @@ namespace Barotrauma } return Priority; } - // Ignore items that are being repaired by someone else. - if (Item.Repairables.Any(r => r.CurrentFixer != null && r.CurrentFixer != character)) + if (HumanAIController.IsItemRepairedByAnother(Item, out _)) { Priority = 0; + IsCompleted = true; } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs index 92d9aa6c8..f549b42c0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs @@ -16,7 +16,7 @@ namespace Barotrauma /// public string RelevantSkill; - private readonly Item prioritizedItem; + public Item PrioritizedItem { get; private set; } public override bool AllowMultipleInstances => true; public override bool AllowInAnySub => true; @@ -28,7 +28,7 @@ namespace Barotrauma public AIObjectiveRepairItems(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1, Item prioritizedItem = null) : base(character, objectiveManager, priorityModifier) { - this.prioritizedItem = prioritizedItem; + PrioritizedItem = prioritizedItem; } protected override void CreateObjectives() @@ -76,7 +76,7 @@ namespace Barotrauma { if (item.Repairables.None(r => r.requiredSkills.Any(s => s.Identifier.Equals(RelevantSkill, StringComparison.OrdinalIgnoreCase)))) { return false; } } - return true; + return !HumanAIController.IsItemRepairedByAnother(item, out _); } public static bool ViableForRepair(Item item, Character character, HumanAIController humanAIController) @@ -139,7 +139,7 @@ namespace Barotrauma protected override IEnumerable GetList() => Item.ItemList; protected override AIObjective ObjectiveConstructor(Item item) - => new AIObjectiveRepairItem(character, item, objectiveManager, priorityModifier: PriorityModifier, isPriority: item == prioritizedItem); + => new AIObjectiveRepairItem(character, item, objectiveManager, priorityModifier: PriorityModifier, isPriority: item == PrioritizedItem); protected override void OnObjectiveCompleted(AIObjective objective, Item target) => HumanAIController.RemoveTargets(character, target); @@ -147,7 +147,7 @@ namespace Barotrauma public static bool IsValidTarget(Item item, Character character) { if (item == null) { return false; } - if (item.IgnoreByAI) { return false; } + if (item.IgnoreByAI(character)) { return false; } if (!item.IsInteractable(character)) { return false; } if (item.IsFullCondition) { return false; } if (item.CurrentHull == null) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs index c5331d79d..3d3a7acb4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs @@ -495,27 +495,19 @@ namespace Barotrauma if (submarine == null) { return matchingItems; } if (ItemComponentType != null || TargetItems.Length > 0) { - matchingItems = TargetItems.Length > 0 ? - Item.ItemList.FindAll(it => TargetItems.Contains(it.Prefab.Identifier) || it.HasTag(TargetItems)) : - Item.ItemList.FindAll(it => TryGetTargetItemComponent(it, out _)); - if (mustBelongToPlayerSub) + foreach (var item in Item.ItemList) { - matchingItems.RemoveAll(it => it.Submarine?.Info != null && it.Submarine.Info.Type != SubmarineType.Player); - } - matchingItems.RemoveAll(it => it.Submarine != submarine && !submarine.DockedTo.Contains(it.Submarine)); - if (requiredTeam.HasValue) - { - matchingItems.RemoveAll(it => it.Submarine == null || it.Submarine.TeamID != requiredTeam.Value); - } - matchingItems.RemoveAll(it => it.NonInteractable); - if (UseController) - { - matchingItems.RemoveAll(i => i.Components.None(c => c.GetType() == ItemComponentType) && !i.TryFindController(out _)); - } - if (interactableFor != null) - { - matchingItems.RemoveAll(it => !it.IsInteractable(interactableFor) || - (UseController && it.FindController() is Controller c && !c.Item.IsInteractable(interactableFor))); + if (TargetItems.Length > 0 && !TargetItems.Contains(item.Prefab.Identifier) && !item.HasTag(TargetItems)) { continue; } + if (TargetItems.Length == 0 && !TryGetTargetItemComponent(item, out _)) { continue; } + if (mustBelongToPlayerSub && item.Submarine?.Info != null && item.Submarine.Info.Type != SubmarineType.Player) { continue; } + if (item.Submarine != submarine && !submarine.DockedTo.Contains(item.Submarine)) { continue; } + if (requiredTeam.HasValue && (item.Submarine == null || item.Submarine.TeamID != requiredTeam.Value)) { continue; } + if (item.NonInteractable) { continue; } + if (ItemComponentType != null && item.Components.None(c => c.GetType() == ItemComponentType)) { continue; } + Controller controller = null; + if (UseController && !item.TryFindController(out controller)) { continue; } + if (interactableFor != null && (!item.IsInteractable(interactableFor) || (UseController && !controller.Item.IsInteractable(interactableFor)))) { continue; } + matchingItems.Add(item); } } return matchingItems; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerItem.cs index 7ece6feb7..f588de938 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerItem.cs @@ -24,8 +24,6 @@ namespace Barotrauma return false; } - if (TargetItem.IgnoreByAI) { return false; } - return true; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index 747e56813..8854ba3d7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -270,14 +270,16 @@ namespace Barotrauma else { LimbJoint rightWrist = GetJointBetweenLimbs(LimbType.RightForearm, LimbType.RightHand); + if (rightWrist != null) + { + forearmLength = Vector2.Distance( + rightElbow.LimbA.type == LimbType.RightForearm ? rightElbow.LocalAnchorA : rightElbow.LocalAnchorB, + rightWrist.LimbA.type == LimbType.RightForearm ? rightWrist.LocalAnchorA : rightWrist.LocalAnchorB); - forearmLength = Vector2.Distance( - rightElbow.LimbA.type == LimbType.RightForearm ? rightElbow.LocalAnchorA : rightElbow.LocalAnchorB, - rightWrist.LimbA.type == LimbType.RightForearm ? rightWrist.LocalAnchorA : rightWrist.LocalAnchorB); - - forearmLength += Vector2.Distance( - rightHand.PullJointLocalAnchorA, - rightElbow.LimbA.type == LimbType.RightHand ? rightElbow.LocalAnchorA : rightElbow.LocalAnchorB); + forearmLength += Vector2.Distance( + rightHand.PullJointLocalAnchorA, + rightElbow.LimbA.type == LimbType.RightHand ? rightElbow.LocalAnchorA : rightElbow.LocalAnchorB); + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 9e2dff4e5..7a21cfbc0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -115,6 +115,8 @@ namespace Barotrauma protected Key[] keys; + public HumanPrefab Prefab; + private CharacterTeamType teamID; public CharacterTeamType TeamID { @@ -1365,11 +1367,27 @@ namespace Barotrauma } } } + private List wearableItems = new List(); public float GetSkillLevel(string skillIdentifier) { if (Info?.Job == null) { return 0.0f; } float skillLevel = Info.Job.GetSkillLevel(skillIdentifier); + + if (skillIdentifier != null) + { + for (int i = 0; i < Inventory.Capacity; i++) + { + if (Inventory.SlotTypes[i] != InvSlotType.Any && Inventory.GetItemAt(i)?.GetComponent() is Wearable wearable) + { + if (wearable.SkillModifiers.TryGetValue(skillIdentifier, out float skillValue)) + { + skillLevel += skillValue; + } + } + } + } + foreach (Affliction affliction in CharacterHealth.GetAllAfflictions()) { skillLevel *= affliction.GetSkillMultiplier(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs index 6d99e68df..ab8ec65ef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs @@ -77,7 +77,7 @@ namespace Barotrauma else if (Strength < ActiveThreshold) { DeactivateHusk(); - if (Prefab is AfflictionPrefabHusk { CauseSpeechImpediment: false }) + if (Prefab is AfflictionPrefabHusk { CauseSpeechImpediment: true }) { character.SpeechImpediment = 100; } @@ -131,7 +131,7 @@ namespace Barotrauma character.NeedsAir = false; } - if (Prefab is AfflictionPrefabHusk { CauseSpeechImpediment: false }) + if (Prefab is AfflictionPrefabHusk { CauseSpeechImpediment: true }) { character.SpeechImpediment = 100; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs index 6e930be09..b40c60233 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs @@ -74,6 +74,20 @@ namespace Barotrauma private string rawAfflictionTypeString; private string[] parsedAfflictionIdentifiers; private string[] parsedAfflictionTypes; + public string[] ParsedAfflictionIdentifiers + { + get + { + return parsedAfflictionIdentifiers; + } + } + public string[] ParsedAfflictionTypes + { + get + { + return parsedAfflictionTypes; + } + } public DamageModifier(XElement element, string parentDebugName) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs index 81885bd92..f87c976ed 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs @@ -167,9 +167,9 @@ namespace Barotrauma } } - public CharacterInfo GetCharacterInfo() + public CharacterInfo GetCharacterInfo(Rand.RandSync randSync = Rand.RandSync.Unsynced) { - var characterElement = ToolBox.SelectWeightedRandom(CustomNPCSets.Keys.ToList(), CustomNPCSets.Values.ToList(), Rand.RandSync.Unsynced); + var characterElement = ToolBox.SelectWeightedRandom(CustomNPCSets.Keys.ToList(), CustomNPCSets.Values.ToList(), randSync); return characterElement != null ? new CharacterInfo(characterElement) : null; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index 9aa766874..40e20c196 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -526,7 +526,7 @@ namespace Barotrauma [Serialize(false, true, description: "Does the character attack when provoked? When enabled, overrides the predefined targeting state with Attack and increases the priority of it."), Editable()] public bool AttackWhenProvoked { get; private set; } - [Serialize(true, true, description: "The character will flee for a brief moment when being shot at if not performing an attack."), Editable] + [Serialize(false, true, description: "The character will flee for a brief moment when being shot at if not performing an attack."), Editable] public bool AvoidGunfire { get; private set; } [Serialize(3f, true, description: "How long the creature avoids gunfire. Also used when the creature is unlatched."), Editable(minValue: 0f, maxValue: 100f)] @@ -565,6 +565,9 @@ namespace Barotrauma [Serialize(0f, true, description: ""), Editable] public float AggressionCumulation { get; private set; } + [Serialize(WallTargetingMethod.Target, true, description: ""), Editable] + public WallTargetingMethod WallTargetingMethod { get; private set; } + public IEnumerable Targets => targets; protected readonly List targets = new List(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index c97e4b208..acaa5db14 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -196,7 +196,7 @@ namespace Barotrauma UpdaterUtil.SaveFileList("filelist.xml"); })); - commands.Add(new Command("spawn|spawncharacter", "spawn [creaturename/jobname] [near/inside/outside/cursor]: Spawn a creature at a random spawnpoint (use the second parameter to only select spawnpoints near/inside/outside the submarine). You can also enter the name of a job (e.g. \"Mechanic\") to spawn a character with a specific job and the appropriate equipment.", null, + commands.Add(new Command("spawn|spawncharacter", "spawn [creaturename/jobname] [near/inside/outside/cursor] [team (0-3)]: Spawn a creature at a random spawnpoint (use the second parameter to only select spawnpoints near/inside/outside the submarine). You can also enter the name of a job (e.g. \"Mechanic\") to spawn a character with a specific job and the appropriate equipment.", null, () => { List characterFiles = GameMain.Instance.GetFilesOfType(ContentType.Character).Select(f => f.Path).ToList(); @@ -1904,9 +1904,19 @@ namespace Barotrauma spawnPoint = WayPoint.GetRandom(human ? SpawnType.Human : SpawnType.Enemy); } - CharacterTeamType teamType; - teamType = args.Length > 2 ? (CharacterTeamType)int.Parse(args[2]) : Character.Controlled != null ? Character.Controlled.TeamID : CharacterTeamType.Team1; if (string.IsNullOrWhiteSpace(args[0])) { return; } + CharacterTeamType teamType = Character.Controlled != null ? Character.Controlled.TeamID : CharacterTeamType.Team1; + if (args.Length > 2) + { + try + { + teamType = (CharacterTeamType)int.Parse(args[2]); + } + catch + { + DebugConsole.ThrowError($"\"{args[2]}\" is not a valid team id."); + } + } if (spawnPoint != null) { spawnPosition = spawnPoint.WorldPosition; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs index 135312e97..4bd734ec5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs @@ -179,7 +179,7 @@ namespace Barotrauma { if (speaker == null) { return; } speaker.CampaignInteractionType = CampaignMode.InteractionType.None; - speaker.ActiveConversation = this; + speaker.ActiveConversation = null; speaker.SetCustomInteract(null, null); #if SERVER GameMain.NetworkMember.CreateEntityEvent(speaker, new object[] { NetEntityEvent.Type.AssignCampaignInteraction }); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs index 1eb6200b1..eaa494848 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs @@ -48,9 +48,9 @@ namespace Barotrauma } else { - foreach (var goToObjective in humanAiController.ObjectiveManager.GetActiveObjectives()) + foreach (var objective in humanAiController.ObjectiveManager.Objectives) { - if (goToObjective.Target == target) + if (objective is AIObjectiveGoTo goToObjective && goToObjective.Target == target) { goToObjective.Abandon = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs index 716427172..93a8e0275 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RemoveItemAction.cs @@ -47,7 +47,7 @@ namespace Barotrauma bool hasValidTargets = false; foreach (Entity target in targets) { - if (target is Character character && character.Inventory != null) + if (target is Character character && character.Inventory != null || target is Item) { hasValidTargets = true; break; @@ -55,20 +55,31 @@ namespace Barotrauma } if (!hasValidTargets) { return; } - List usedItems = new List(); + HashSet removedItems = new HashSet(); foreach (Entity target in targets) { Inventory inventory = (target as Character)?.Inventory; - if (inventory == null) { continue; } - while (usedItems.Count < Amount) + if (inventory != null) { - var item = inventory.FindItem(it => - it != null && - !usedItems.Contains(it) && - it.Prefab.Identifier.Equals(ItemIdentifier, StringComparison.InvariantCultureIgnoreCase), recursive: true); - if (item == null) { break; } - Entity.Spawner.AddToRemoveQueue(item); - usedItems.Add(item); + while (removedItems.Count < Amount) + { + var item = inventory.FindItem(it => + it != null && + !removedItems.Contains(it) && + it.Prefab.Identifier.Equals(ItemIdentifier, StringComparison.InvariantCultureIgnoreCase), recursive: true); + if (item == null) { break; } + Entity.Spawner.AddToRemoveQueue(item); + removedItems.Add(item); + } + } + else if (target is Item item) + { + if (item.Prefab.Identifier.Equals(ItemIdentifier, StringComparison.InvariantCultureIgnoreCase)) + { + Entity.Spawner.AddToRemoveQueue(item); + removedItems.Add(item); + if (removedItems.Count >= Amount) { break; } + } } } isFinished = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs index d78d9d2ca..4737eba63 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs @@ -107,27 +107,32 @@ namespace Barotrauma if (!string.IsNullOrEmpty(NPCSetIdentifier) && !string.IsNullOrEmpty(NPCIdentifier)) { HumanPrefab humanPrefab = NPCSet.Get(NPCSetIdentifier, NPCIdentifier); - ISpatialEntity spawnPos = GetSpawnPos(); - Entity.Spawner.AddToSpawnQueue(CharacterPrefab.HumanSpeciesName, OffsetSpawnPos(spawnPos?.WorldPosition ?? Vector2.Zero, 100.0f), humanPrefab.GetCharacterInfo(), onSpawn: newCharacter => + if (humanPrefab != null) { - newCharacter.TeamID = CharacterTeamType.FriendlyNPC; - newCharacter.EnableDespawn = false; - humanPrefab.GiveItems(newCharacter, newCharacter.Submarine); - if (LootingIsStealing) + ISpatialEntity spawnPos = GetSpawnPos(); + Entity.Spawner.AddToSpawnQueue(CharacterPrefab.HumanSpeciesName, OffsetSpawnPos(spawnPos?.WorldPosition ?? Vector2.Zero, 100.0f), humanPrefab.GetCharacterInfo(), onSpawn: newCharacter => { - foreach (Item item in newCharacter.Inventory.AllItems) + if (newCharacter == null) { return; } + newCharacter.Prefab = humanPrefab; + newCharacter.TeamID = CharacterTeamType.FriendlyNPC; + newCharacter.EnableDespawn = false; + humanPrefab.GiveItems(newCharacter, newCharacter.Submarine); + if (LootingIsStealing) { - item.SpawnedInOutpost = true; - item.AllowStealing = false; + foreach (Item item in newCharacter.Inventory.AllItems) + { + item.SpawnedInOutpost = true; + item.AllowStealing = false; + } } - } - humanPrefab.InitializeCharacter(newCharacter, spawnPos); - if (!string.IsNullOrEmpty(TargetTag) && newCharacter != null) - { - ParentEvent.AddTarget(TargetTag, newCharacter); - } - spawnedEntity = newCharacter; - }); + humanPrefab.InitializeCharacter(newCharacter, spawnPos); + if (!string.IsNullOrEmpty(TargetTag) && newCharacter != null) + { + ParentEvent.AddTarget(TargetTag, newCharacter); + } + spawnedEntity = newCharacter; + }); + } } else if (!string.IsNullOrEmpty(SpeciesName)) { @@ -197,8 +202,7 @@ namespace Barotrauma } } - spawned = true; - + spawned = true; } public static Vector2 OffsetSpawnPos(Vector2 pos, float offsetAmount) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs index ef22c7638..c96723441 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs @@ -6,7 +6,7 @@ namespace Barotrauma { class TagAction : EventAction { - public enum SubType { Any= 0, Player = 1, Outpost = 2, Wreck = 4, BeaconStation = 8 } + public enum SubType { Any = 0, Player = 1, Outpost = 2, Wreck = 4, BeaconStation = 8 } [Serialize("", true)] public string Criteria { get; set; } @@ -67,6 +67,16 @@ namespace Barotrauma #endif } + private void TagHumansByIdentifier(string identifier) + { + foreach (Character c in Character.CharacterList) + { + if (c.Prefab?.Identifier.Equals(identifier, StringComparison.OrdinalIgnoreCase) ?? false) + { + ParentEvent.AddTarget(Tag, c); + } + } + } private void TagStructuresByIdentifier(string identifier) { ParentEvent.AddTargetPredicate(Tag, e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.Prefab.Identifier.Equals(identifier, StringComparison.InvariantCultureIgnoreCase)); @@ -122,6 +132,9 @@ namespace Barotrauma case "crew": TagCrew(); break; + case "humanprefabidentifier": + if (kvp.Length > 1) { TagHumansByIdentifier(kvp[1].Trim()); } + break; case "structureidentifier": if (kvp.Length > 1) { TagStructuresByIdentifier(kvp[1].Trim()); } break; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs index 22d5bc143..f8749155b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs @@ -122,6 +122,7 @@ namespace Barotrauma { npcOrItem = npc; npc.CampaignInteractionType = CampaignMode.InteractionType.Examine; + npc.RequireConsciousnessForCustomInteract = false; #if CLIENT npc.SetCustomInteract( (speaker, player) => { if (e1 == speaker) { Trigger(speaker, player); } else { Trigger(player, speaker); } }, @@ -132,7 +133,6 @@ namespace Barotrauma TextManager.Get("CampaignInteraction.Talk")); GameMain.NetworkMember.CreateEntityEvent(npc, new object[] { NetEntityEvent.Type.AssignCampaignInteraction }); #endif - npc.RequireConsciousnessForCustomInteract = false; } return; @@ -176,6 +176,9 @@ namespace Barotrauma npc.CampaignInteractionType = CampaignMode.InteractionType.None; npc.SetCustomInteract(null, null); npc.RequireConsciousnessForCustomInteract = true; +#if SERVER + GameMain.NetworkMember.CreateEntityEvent(npc, new object[] { NetEntityEvent.Type.AssignCampaignInteraction }); +#endif } else if (npcOrItem.TryGet(out Item item)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index da945d940..3ede84eeb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -168,7 +168,7 @@ namespace Barotrauma if (eventSet == null) { return; } if (eventSet.OncePerOutpost) { - foreach (EventPrefab ep in eventSet.EventPrefabs.Select(e => e.First)) + foreach (EventPrefab ep in eventSet.EventPrefabs.Select(e => e.prefab)) { if (!level.LevelData.NonRepeatableEvents.Contains(ep)) { @@ -374,11 +374,11 @@ namespace Barotrauma preloadedSprites.Clear(); } - private float CalculateCommonness(Pair eventPrefab) + private float CalculateCommonness(EventPrefab eventPrefab, float baseCommonness) { - if (level.LevelData.NonRepeatableEvents.Contains(eventPrefab.First)) { return 0.0f; } - float retVal = eventPrefab.Second; - if (level.LevelData.EventHistory.Contains(eventPrefab.First)) { retVal *= 0.1f; } + if (level.LevelData.NonRepeatableEvents.Contains(eventPrefab)) { return 0.0f; } + float retVal = baseCommonness; + if (level.LevelData.EventHistory.Contains(eventPrefab)) { retVal *= 0.1f; } return retVal; } @@ -420,8 +420,8 @@ namespace Barotrauma } var suitablePrefabs = eventSet.EventPrefabs.FindAll(e => - string.IsNullOrEmpty(e.First.BiomeIdentifier) || - e.First.BiomeIdentifier.Equals(level.LevelData?.Biome?.Identifier, StringComparison.OrdinalIgnoreCase)); + string.IsNullOrEmpty(e.prefab.BiomeIdentifier) || + e.prefab.BiomeIdentifier.Equals(level.LevelData?.Biome?.Identifier, StringComparison.OrdinalIgnoreCase)); for (int i = 0; i < applyCount; i++) { @@ -429,14 +429,14 @@ namespace Barotrauma { if (suitablePrefabs.Count > 0) { - List> unusedEvents = new List>(suitablePrefabs); + var unusedEvents = new List<(EventPrefab prefab, float commonness, float probability)>(suitablePrefabs); for (int j = 0; j < eventSet.EventCount; j++) { - if (unusedEvents.All(e => CalculateCommonness(e) <= 0.0f)) { break; } - var eventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, unusedEvents.Select(e => CalculateCommonness(e)).ToList(), rand); - if (eventPrefab != null) + if (unusedEvents.All(e => CalculateCommonness(e.prefab, e.commonness) <= 0.0f)) { break; } + (EventPrefab eventPrefab, float commonness, float probability) = ToolBox.SelectWeightedRandom(unusedEvents, unusedEvents.Select(e => CalculateCommonness(e.prefab, e.commonness)).ToList(), rand); + if (eventPrefab != null && rand.NextDouble() <= probability) { - var newEvent = eventPrefab.First.CreateInstance(); + var newEvent = eventPrefab.CreateInstance(); if (newEvent == null) { continue; } newEvent.Init(true); if (i < spawnPosFilter.Count) { newEvent.SpawnPosFilter = spawnPosFilter[i]; } @@ -450,7 +450,7 @@ namespace Barotrauma selectedEvents.Add(eventSet, new List()); } selectedEvents[eventSet].Add(newEvent); - unusedEvents.Remove(eventPrefab); + unusedEvents.Remove((eventPrefab, commonness, probability)); } } } @@ -465,9 +465,10 @@ namespace Barotrauma } else { - foreach (Pair eventPrefab in suitablePrefabs) + foreach ((EventPrefab eventPrefab, float commonness, float probability) in suitablePrefabs) { - var newEvent = eventPrefab.First.CreateInstance(); + if (rand.NextDouble() > probability) { continue; } + var newEvent = eventPrefab.CreateInstance(); if (newEvent == null) { continue; } newEvent.Init(true); #if DEBUG diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs index d605651cc..b7632a54e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs @@ -8,7 +8,7 @@ namespace Barotrauma { public readonly XElement ConfigElement; public readonly Type EventType; - public readonly float SpawnProbability; + public readonly float Probability; public readonly bool TriggerEventCooldown; public float Commonness; public string Identifier; @@ -39,7 +39,7 @@ namespace Barotrauma Identifier = ConfigElement.GetAttributeString("identifier", string.Empty); BiomeIdentifier = ConfigElement.GetAttributeString("biome", string.Empty); Commonness = element.GetAttributeFloat("commonness", 1.0f); - SpawnProbability = Math.Clamp(element.GetAttributeFloat("spawnprobability", 1.0f), 0, 1); + Probability = Math.Clamp(element.GetAttributeFloat(1.0f, "probability", "spawnprobability"), 0, 1); TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", true); UnlockPathEvent = element.GetAttributeBool("unlockpathevent", false); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs index 080b84dcb..7f5ee22a5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs @@ -48,10 +48,10 @@ namespace Barotrauma List eventPrefabs = new List(PrefabList); foreach (var eventSet in List) { - eventPrefabs.AddRange(eventSet.EventPrefabs.Select(ep => ep.First)); + eventPrefabs.AddRange(eventSet.EventPrefabs.Select(ep => ep.prefab)); foreach (var childSet in eventSet.ChildSets) { - eventPrefabs.AddRange(childSet.EventPrefabs.Select(ep => ep.First)); + eventPrefabs.AddRange(childSet.EventPrefabs.Select(ep => ep.prefab)); } } return eventPrefabs; @@ -96,8 +96,7 @@ namespace Barotrauma public readonly Dictionary Commonness; - //Pair.First: event prefab, Pair.Second: commonness - public readonly List> EventPrefabs; + public readonly List<(EventPrefab prefab, float commonness, float probability)> EventPrefabs; public readonly List ChildSets; @@ -111,7 +110,7 @@ namespace Barotrauma { DebugIdentifier = element.GetAttributeString("identifier", null) ?? debugIdentifier; Commonness = new Dictionary(); - EventPrefabs = new List>(); + EventPrefabs = new List<(EventPrefab prefab, float commonness, float probability)>(); ChildSets = new List(); BiomeIdentifier = element.GetAttributeString("biome", string.Empty); @@ -184,13 +183,14 @@ namespace Barotrauma else { float commonness = subElement.GetAttributeFloat("commonness", prefab.Commonness); - EventPrefabs.Add(new Pair( prefab, commonness)); + float probability = subElement.GetAttributeFloat("probability", prefab.Probability); + EventPrefabs.Add((prefab, commonness, probability)); } } else { var prefab = new EventPrefab(subElement); - EventPrefabs.Add(new Pair(prefab, prefab.Commonness)); + EventPrefabs.Add((prefab, prefab.Commonness, prefab.Probability)); } break; } @@ -342,13 +342,13 @@ namespace Barotrauma { if (thisSet.ChooseRandom) { - List> unusedEvents = new List>(thisSet.EventPrefabs); + var unusedEvents = new List<(EventPrefab prefab, float commonness, float probability)>(thisSet.EventPrefabs); for (int i = 0; i < thisSet.EventCount; i++) { - var eventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, unusedEvents.Select(e => e.Second).ToList(), Rand.RandSync.Unsynced); - if (eventPrefab != null) + var eventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, unusedEvents.Select(e => e.commonness).ToList(), Rand.RandSync.Unsynced); + if (eventPrefab.prefab != null) { - AddEvent(stats, eventPrefab.First); + AddEvent(stats, eventPrefab.prefab); unusedEvents.Remove(eventPrefab); } } @@ -357,7 +357,7 @@ namespace Barotrauma { foreach (var eventPrefab in thisSet.EventPrefabs) { - AddEvent(stats, eventPrefab.First); + AddEvent(stats, eventPrefab.prefab); } } foreach (var childSet in thisSet.ChildSets) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs index fc2531528..6adc55c57 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs @@ -24,6 +24,19 @@ namespace Barotrauma private Submarine sub; + public override string Description + { + get + { + if (Submarine.MainSub != sub) + { + string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", GetReward(Submarine.MainSub))}‖end‖"; + if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); } + } + return description; + } + } + public CargoMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs index 7e9dc916c..a369534b1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/EscortMission.cs @@ -14,12 +14,15 @@ namespace Barotrauma private readonly XElement itemConfig; private readonly List characters = new List(); - private readonly Dictionary> characterDictionary = new Dictionary>(); + private readonly Dictionary> characterItems = new Dictionary>(); private readonly int baseEscortedCharacters; private readonly float scalingEscortedCharacters; private readonly float terroristChance; + private int calculatedReward; + private Submarine missionSub; + private Character vipCharacter; private readonly List terroristCharacters = new List(); @@ -30,24 +33,43 @@ namespace Barotrauma public EscortMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) { + missionSub = sub; characterConfig = prefab.ConfigElement.Element("Characters"); - // Should reflect different escortables, prisoners, VIPs, passengers (where does this comment refer to?) - baseEscortedCharacters = prefab.ConfigElement.GetAttributeInt("baseescortedcharacters", 1); scalingEscortedCharacters = prefab.ConfigElement.GetAttributeFloat("scalingescortedcharacters", 0); terroristChance = prefab.ConfigElement.GetAttributeFloat("terroristchance", 0); itemConfig = prefab.ConfigElement.Element("TerroristItems"); + CalculateReward(); + } + + private void CalculateReward() + { + if (missionSub == null) + { + calculatedReward = Prefab.Reward; + return; + } + + int multiplier = CalculateScalingEscortedCharacterCount(); + calculatedReward = Prefab.Reward * multiplier; + + string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", GetReward(missionSub))}‖end‖"; + if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); } } public override int GetReward(Submarine sub) { - int multiplier = CalculateScalingEscortedCharacterCount(); - return Prefab.Reward * multiplier; + if (sub != missionSub) + { + missionSub = sub; + CalculateReward(); + } + return calculatedReward; } int CalculateScalingEscortedCharacterCount(bool inMission = false) { - if (Submarine.MainSub == null || Submarine.MainSub.Info == null) // UI logic failing to get the correct value is not important, but the mission logic must succeed + if (missionSub == null || missionSub.Info == null) // UI logic failing to get the correct value is not important, but the mission logic must succeed { if (inMission) { @@ -55,13 +77,13 @@ namespace Barotrauma } return 1; } - return (int)Math.Round(baseEscortedCharacters + scalingEscortedCharacters * (Submarine.MainSub.Info.RecommendedCrewSizeMin + Submarine.MainSub.Info.RecommendedCrewSizeMax) / 2); + return (int)Math.Round(baseEscortedCharacters + scalingEscortedCharacters * (missionSub.Info.RecommendedCrewSizeMin + missionSub.Info.RecommendedCrewSizeMax) / 2); } private void InitEscort() { characters.Clear(); - characterDictionary.Clear(); + characterItems.Clear(); // VIP transport mission characters stay in the same location; other characters roam at will // could be replaced with a designated waypoint for VIPs, such as cargo or crew WayPoint explicitStayInHullPos = WayPoint.GetRandom(SpawnType.Human, null, Submarine.MainSub); @@ -78,7 +100,7 @@ namespace Barotrauma int count = CalculateScalingEscortedCharacterCount(inMission: true); for (int i = 0; i < count; i++) { - Character spawnedCharacter = CreateHuman(CreateHumanPrefabFromElement(element), characters, characterDictionary, Submarine.MainSub, CharacterTeamType.FriendlyNPC, explicitStayInHullPos, humanPrefabRandSync: randSync); + Character spawnedCharacter = CreateHuman(CreateHumanPrefabFromElement(element), characters, characterItems, Submarine.MainSub, CharacterTeamType.FriendlyNPC, explicitStayInHullPos, humanPrefabRandSync: randSync); if (spawnedCharacter.AIController is HumanAIController humanAI) { humanAI.InitMentalStateManager(); @@ -156,6 +178,13 @@ namespace Barotrauma return; } + // to ensure single missions run without issues, default to mainsub + if (missionSub == null) + { + missionSub = Submarine.MainSub; + CalculateReward(); + } + if (!IsClient) { InitEscort(); @@ -276,7 +305,7 @@ namespace Barotrauma // characters that survived will take their items with them, in case players tried to be crafty and steal them // this needs to run here in case players abort the mission by going back home // TODO: I think this might feel like a bug. - foreach (var characterItem in characterDictionary) + foreach (var characterItem in characterItems) { if (Survived(characterItem.Key) || !completed) { @@ -291,7 +320,7 @@ namespace Barotrauma } characters.Clear(); - characterDictionary.Clear(); + characterItems.Clear(); failed = !completed; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index bd904048e..ce652ed59 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -400,8 +400,6 @@ namespace Barotrauma // putting these here since both escort and pirate missions need them. could be tucked away into another class that they can inherit from (or use composition) protected HumanPrefab CreateHumanPrefabFromElement(XElement element) { - HumanPrefab humanPrefab = null; - if (element.Attribute("name") != null) { DebugConsole.ThrowError("Error in mission \"" + Name + "\" - use character identifiers instead of names to configure the characters."); @@ -411,8 +409,7 @@ namespace Barotrauma string characterIdentifier = element.GetAttributeString("identifier", ""); string characterFrom = element.GetAttributeString("from", ""); - humanPrefab = NPCSet.Get(characterFrom, characterIdentifier); - + HumanPrefab humanPrefab = NPCSet.Get(characterFrom, characterIdentifier); if (humanPrefab == null) { DebugConsole.ThrowError("Couldn't spawn character for mission: character prefab \"" + characterIdentifier + "\" not found"); @@ -428,9 +425,11 @@ namespace Barotrauma { positionToStayIn = WayPoint.GetRandom(SpawnType.Human, null, submarine); } - var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, npcIdentifier: humanPrefab.Identifier, jobPrefab: humanPrefab.GetJobPrefab(humanPrefabRandSync), randSync: humanPrefabRandSync); + + var characterInfo = humanPrefab.GetCharacterInfo(Rand.RandSync.Server) ?? new CharacterInfo(CharacterPrefab.HumanSpeciesName, npcIdentifier: humanPrefab.Identifier, jobPrefab: humanPrefab.GetJobPrefab(humanPrefabRandSync), randSync: humanPrefabRandSync); characterInfo.TeamID = teamType; Character spawnedCharacter = Character.Create(characterInfo.SpeciesName, positionToStayIn.WorldPosition, ToolBox.RandomSeed(8), characterInfo, createNetworkEvent: false); + spawnedCharacter.Prefab = humanPrefab; humanPrefab.InitializeCharacter(spawnedCharacter, positionToStayIn); humanPrefab.GiveItems(spawnedCharacter, submarine, Rand.RandSync.Server, createNetworkEvents: false); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs index 5e6adf829..31f96efbe 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs @@ -21,7 +21,7 @@ namespace Barotrauma private Submarine enemySub; private readonly List characters = new List(); - private readonly Dictionary> characterDictionary = new Dictionary>(); + private readonly Dictionary> characterItems = new Dictionary>(); // Update the last sighting periodically so that the players can find the pirate sub even if they have lost the track of it. private readonly float pirateSightingUpdateFrequency = 30; @@ -103,17 +103,20 @@ namespace Barotrauma alternateReward = submarineConfig.GetAttributeInt("alternatereward", Reward); - string submarineIdentifier = submarineConfig.GetAttributeString("identifier", string.Empty); - if (submarineIdentifier == string.Empty) + string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", alternateReward)}‖end‖"; + if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); } + + string submarinePath = submarineConfig.GetAttributeString("path", string.Empty); + if (submarinePath == string.Empty) { - DebugConsole.ThrowError("No identifier used for submarine for pirate mission!"); + DebugConsole.ThrowError($"No path used for submarine for the pirate mission \"{Prefab.Identifier}\"!"); return; } // maybe a little redundant - var contentFile = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.EnemySubmarine).FirstOrDefault(x => x.Path == submarineIdentifier); + var contentFile = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.EnemySubmarine).FirstOrDefault(x => x.Path == submarinePath); if (contentFile == null) { - DebugConsole.ThrowError("No submarine file found with the identifier!"); + DebugConsole.ThrowError($"No submarine file found from the path {submarinePath}!"); return; } @@ -187,14 +190,13 @@ namespace Barotrauma reactor.PowerUpImmediately(); } enemySub.EnableMaintainPosition(); - enemySub.SetPosition(spawnPos); enemySub.TeamID = CharacterTeamType.None; } private void InitPirates() { characters.Clear(); - characterDictionary.Clear(); + characterItems.Clear(); if (characterConfig == null) { @@ -222,13 +224,13 @@ namespace Barotrauma if (characterType == null) { - DebugConsole.ThrowError("No character types defined in CharacterTypes for a declared type identifier in mission file " + this); + DebugConsole.ThrowError($"No character types defined in CharacterTypes for a declared type identifier in mission \"{Prefab.Identifier}\"."); return; } XElement variantElement = GetRandomDifficultyModifiedElement(characterType, enemyCreationDifficulty, RandomnessModifier); - Character spawnedCharacter = CreateHuman(CreateHumanPrefabFromElement(variantElement), characters, characterDictionary, enemySub, CharacterTeamType.None, null); + Character spawnedCharacter = CreateHuman(CreateHumanPrefabFromElement(variantElement), characters, characterItems, enemySub, CharacterTeamType.None, null); if (!commanderAssigned) { bool isCommander = variantElement.GetAttributeBool("iscommander", false); @@ -242,6 +244,14 @@ namespace Barotrauma commanderAssigned = true; } } + + foreach (Item item in spawnedCharacter.Inventory.AllItems) + { + if (item?.Prefab.Identifier == "idcard") + { + item.AddTag("id_pirate"); + } + } } } } @@ -297,9 +307,10 @@ namespace Barotrauma { InitPirateShip(spawnPos); } + enemySub.SetPosition(spawnPos); - // flipping the sub on the frame it is moved into place must be done after it's been moved, or it breaks item connections to the submarine - // creating the pirates have to be done after the sub has been flipped, or it seems to break the AI pathing + // flipping the sub on the frame it is moved into place must be done after it's been moved, or it breaks item connections in the submarine + // creating the pirates has to be done after the sub has been flipped, or it seems to break the AI pathing enemySub.FlipX(); enemySub.ShowSonarMarker = false; @@ -375,7 +386,7 @@ namespace Barotrauma completed = true; } characters.Clear(); - characterDictionary.Clear(); + characterItems.Clear(); failed = !completed; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs index 98887847b..d1f63d6a1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs @@ -182,13 +182,6 @@ namespace Barotrauma { if (disallowed) { return; } - if (Rand.Value(Rand.RandSync.Server) > prefab.SpawnProbability) - { - spawnPos = null; - Finished(); - return; - } - spawnPos = Vector2.Zero; var availablePositions = GetAvailableSpawnPositions(); var chosenPosition = new Level.InterestingPosition(Point.Zero, Level.PositionType.MainPath, isValid: false); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs index f2e62e40d..c93b28077 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs @@ -207,7 +207,8 @@ namespace Barotrauma SpawnedInOutpost = validContainer.Key.Item.SpawnedInOutpost, AllowStealing = validContainer.Key.Item.AllowStealing, OriginalModuleIndex = validContainer.Key.Item.OriginalModuleIndex, - OriginalContainerID = validContainer.Key.Item.ID + OriginalContainerIndex = + Item.ItemList.Where(it => it.Submarine == validContainer.Key.Item.Submarine && it.OriginalModuleIndex == validContainer.Key.Item.OriginalModuleIndex).ToList().IndexOf(validContainer.Key.Item) }; foreach (WifiComponent wifiComponent in item.GetComponents()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs index cb4ec388a..c63d385d8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs @@ -246,42 +246,21 @@ namespace Barotrauma continue; } availableContainers.Add(itemContainer); - #if SERVER +#if SERVER if (GameMain.Server != null) { Entity.Spawner.CreateNetworkEvent(itemContainer.Item, false); } - #endif - } - } - - if (itemContainer == null) - { - //no container, place at the waypoint - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) - { - Entity.Spawner.AddToSpawnQueue(pi.ItemPrefab, position, wp.Submarine, onSpawned: itemSpawned); +#endif } - else - { - var item = new Item(pi.ItemPrefab, position, wp.Submarine); - itemSpawned(item); - } - continue; - } - - //place in the container - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) - { - Entity.Spawner.AddToSpawnQueue(pi.ItemPrefab, itemContainer.Inventory, onSpawned: itemSpawned); - } - else - { - var item = new Item(pi.ItemPrefab, position, wp.Submarine); - itemContainer.Inventory.TryPutItem(item, null); - itemSpawned(item); } + var item = new Item(pi.ItemPrefab, position, wp.Submarine); + itemContainer?.Inventory.TryPutItem(item, null); + itemSpawned(item); +#if SERVER + Entity.Spawner?.CreateNetworkEvent(item, false); +#endif static void itemSpawned(Item item) { Submarine sub = item.Submarine ?? item.GetRootContainer()?.Submarine; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs index b80599722..757b03269 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs @@ -412,16 +412,16 @@ namespace Barotrauma public static Character GetCharacterForQuickAssignment(Order order, Character controlledCharacter, IEnumerable characters, bool includeSelf = false) { - var controllingCharacter = controlledCharacter != null; + bool isControlledCharacterNull = controlledCharacter == null; #if !DEBUG - if (!controllingCharacter) { return null; } + if (isControlledCharacterNull) { return null; } #endif - if (order.Category == OrderCategory.Operate && HumanAIController.IsItemOperatedByAnother(null, order.TargetItemComponent, out Character operatingCharacter) && - (!controllingCharacter || operatingCharacter.CanHearCharacter(controlledCharacter))) + if (order.Category == OrderCategory.Operate && HumanAIController.IsItemTargetedBySomeone(order.TargetItemComponent, controlledCharacter != null ? controlledCharacter.TeamID : CharacterTeamType.Team1, out Character operatingCharacter) && + (isControlledCharacterNull || operatingCharacter.CanHearCharacter(controlledCharacter))) { return operatingCharacter; } - return GetCharactersSortedForOrder(order, characters, controlledCharacter, includeSelf).FirstOrDefault(c => !controllingCharacter || c.CanHearCharacter(controlledCharacter)) ?? controlledCharacter; + return GetCharactersSortedForOrder(order, characters, controlledCharacter, includeSelf).FirstOrDefault(c => isControlledCharacterNull || c.CanHearCharacter(controlledCharacter)) ?? controlledCharacter; } public static IEnumerable GetCharactersSortedForOrder(Order order, IEnumerable characters, Character controlledCharacter, bool includeSelf, IEnumerable extraCharacters = null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index 21abf1111..e5a5dab98 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -494,7 +494,7 @@ namespace Barotrauma if (Level.Loaded.StartOutpost.DockedTo.Any()) { var dockedSub = Level.Loaded.StartOutpost.DockedTo.FirstOrDefault(); - if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle) { return null; } + if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != leavingPlayers.FirstOrDefault()?.TeamID) { return null; } return dockedSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : dockedSub; } @@ -522,7 +522,7 @@ namespace Barotrauma if (Level.Loaded.EndOutpost.DockedTo.Any()) { var dockedSub = Level.Loaded.EndOutpost.DockedTo.FirstOrDefault(); - if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle) { return null; } + if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != leavingPlayers.FirstOrDefault()?.TeamID) { return null; } return dockedSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : dockedSub; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index 7876a4b1a..428a3e21f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -549,7 +549,10 @@ namespace Barotrauma if (port.IsHorizontal || port.Docked) { continue; } if (port.Item.Submarine == level.StartOutpost) { - outPostPort = port; + if (port.DockingTarget == null) + { + outPostPort = port; + } continue; } if (port.Item.Submarine != Submarine) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs index ecd468277..9148c6102 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs @@ -323,7 +323,11 @@ namespace Barotrauma Campaign.Money -= price; itemToRemove.AvailableSwaps.Add(itemToRemove.Prefab); - if (itemToInstall != null) { itemToRemove.AvailableSwaps.Add(itemToInstall); } + if (itemToInstall != null && !itemToRemove.AvailableSwaps.Contains(itemToInstall)) + { + itemToRemove.PurchasedNewSwap = true; + itemToRemove.AvailableSwaps.Add(itemToInstall); + } if (itemToRemove.Prefab != itemToInstall && itemToInstall != null) { @@ -424,7 +428,12 @@ namespace Barotrauma List pendingUpgrades = PendingUpgrades; - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) + if (Level.Loaded is { Type: LevelData.LevelType.Outpost }) + { + return; + } + + if (GameMain.NetworkMember is { IsClient: true }) { if (loadedUpgrades != null) { @@ -438,7 +447,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(level, 0, prefab.MaxLevel)); + SetUpgradeLevel(prefab, category, Math.Clamp(GetRealUpgradeLevel(prefab, category) + level, 0, prefab.MaxLevel)); } PendingUpgrades.Clear(); @@ -703,7 +712,7 @@ namespace Barotrauma private void LoadPendingUpgrades(XElement? element, bool isSingleplayer = true) { - if (element == null || !element.HasElements) { return; } + if (!(element is { HasElements: true })) { return; } List pendingUpgrades = new List(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs index 73edb3c68..4488c262b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs @@ -538,16 +538,18 @@ namespace Barotrauma.Items.Components { for (int i = 0; i < 2; i++) { - if (hull.Submarine != subs[i]) continue; - if (hull.WorldRect.Y < hullRects[i].Y - hullRects[i].Height) continue; - if (hull.WorldRect.Y - hull.WorldRect.Height > hullRects[i].Y) continue; + if (hull.Submarine != subs[i]) { continue; } + if (hull.WorldRect.Y - 5 < hullRects[i].Y - hullRects[i].Height) { continue; } + if (hull.WorldRect.Y - hull.WorldRect.Height + 5 > hullRects[i].Y) { continue; } if (i == 0) //left hull { + if (hull.WorldPosition.X > hullRects[0].Center.X) { continue; } leftSubRightSide = Math.Max(hull.WorldRect.Right, leftSubRightSide); } else //upper hull { + if (hull.WorldPosition.X < hullRects[1].Center.X) { continue; } rightSubLeftSide = Math.Min(hull.WorldRect.X, rightSubLeftSide); } } @@ -591,8 +593,11 @@ namespace Barotrauma.Items.Components } } + int expand = 5; for (int i = 0; i < 2; i++) { + hullRects[i].X -= expand; + hullRects[i].Width += expand * 2; hullRects[i].Location -= MathUtils.ToPoint((subs[i].WorldPosition - subs[i].HiddenSubPosition)); hulls[i] = new Hull(MapEntityPrefab.Find(null, "hull"), hullRects[i], subs[i]); hulls[i].AddToGrid(subs[i]); @@ -636,16 +641,18 @@ namespace Barotrauma.Items.Components { for (int i = 0; i < 2; i++) { - if (hull.Submarine != subs[i]) continue; - if (hull.WorldRect.Right < hullRects[i].X) continue; - if (hull.WorldRect.X > hullRects[i].Right) continue; + if (hull.Submarine != subs[i]) { continue; } + if (hull.WorldRect.Right - 5 < hullRects[i].X) { continue; } + if (hull.WorldRect.X + 5 > hullRects[i].Right) { continue; } if (i == 0) //lower hull { + if (hull.WorldPosition.Y > hullRects[i].Y - hullRects[i].Height / 2) { continue; } lowerSubTop = Math.Max(hull.WorldRect.Y, lowerSubTop); } else //upper hull { + if (hull.WorldPosition.Y < hullRects[i].Y - hullRects[i].Height / 2) { continue; } upperSubBottom = Math.Min(hull.WorldRect.Y - hull.WorldRect.Height, upperSubBottom); } } @@ -705,8 +712,11 @@ namespace Barotrauma.Items.Components } + int expand = 5; for (int i = 0; i < 2; i++) { + hullRects[i].Y += expand; + hullRects[i].Height += expand * 2; hullRects[i].Location -= MathUtils.ToPoint((subs[i].WorldPosition - subs[i].HiddenSubPosition)); hulls[i] = new Hull(MapEntityPrefab.Find(null, "hull"), hullRects[i], subs[i]); hulls[i].AddToGrid(subs[i]); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs index 6c0e1e608..99927ca74 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs @@ -663,7 +663,7 @@ namespace Barotrauma.Items.Components } else { - return Item.GetConnectedComponents(true).Any(b => b.HasAccess(character)); + return base.HasAccess(character) && Item.GetConnectedComponents(true).Any(b => b.HasAccess(character)); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs index 5b48a61e2..8a73e00a7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs @@ -69,6 +69,7 @@ namespace Barotrauma.Items.Components item.IsShootable = true; // TODO: should define this in xml if we have ranged weapons that don't require aim to use item.RequireAimToUse = true; + characterUsable = true; InitProjSpecific(element); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index 854adbf28..8365d479f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -660,7 +660,7 @@ namespace Barotrauma.Items.Components /// public virtual bool HasAccess(Character character) { - if (item.IgnoreByAI) { return false; } + if (character.IsBot && item.IgnoreByAI(character)) { return false; } if (!item.IsInteractable(character)) { return false; } if (requiredItems.None()) { return true; } if (character.Inventory != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index 547ecf00d..08d3d33ca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -22,6 +22,8 @@ namespace Barotrauma.Items.Components } } + private bool alwaysContainedItemsSpawned; + public ItemInventory Inventory; private readonly List activeContainedItems = new List(); @@ -208,6 +210,11 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { + if (!string.IsNullOrEmpty(SpawnWithId) && !alwaysContainedItemsSpawned) + { + SpawnAlwaysContainedItems(); + } + if (item.ParentInventory is CharacterInventory) { item.SetContainedItemPositions(); @@ -418,11 +425,13 @@ namespace Barotrauma.Items.Components if (!isEditor && (Entity.Spawner == null || Entity.Spawner.Removed) && GameMain.NetworkMember == null) { var spawnedItem = new Item(prefab, Vector2.Zero, null); - Inventory.TryPutItem(spawnedItem, null, spawnedItem.AllowedSlots, createNetworkEvent: false); + Inventory.TryPutItem(spawnedItem, null, spawnedItem.AllowedSlots, createNetworkEvent: false); + alwaysContainedItemsSpawned = true; } else { - Entity.Spawner?.AddToSpawnQueue(prefab, Inventory, spawnIfInventoryFull: false); + IsActive = true; + Entity.Spawner?.AddToSpawnQueue(prefab, Inventory, spawnIfInventoryFull: false, onSpawned: (Item item) => { alwaysContainedItemsSpawned = true; }); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs index c5fd6c2c0..6c80e925d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs @@ -326,19 +326,24 @@ namespace Barotrauma.Items.Components tolerance = MathHelper.Lerp(5.0f, 20.0f, degreeOfSuccess); allowedTurbineOutput = new Vector2(correctTurbineOutput - tolerance, correctTurbineOutput + tolerance); + DebugConsole.Log($"Degree of success: {degreeOfSuccess}"); + DebugConsole.Log($"Current load: {currentLoad}"); + DebugConsole.Log($"Max power output: {MaxPowerOutput}"); + DebugConsole.Log($"Available fuel: {AvailableFuel}"); + float desiredTurbineOutput = MathHelper.Clamp(correctTurbineOutput, 0.0f, 100.0f); DebugConsole.Log($"Turbine output reset: {targetTurbineOutput}, {turbineOutput} -> {desiredTurbineOutput}"); targetTurbineOutput = desiredTurbineOutput; turbineOutput = desiredTurbineOutput; - float desiredFissionRate = (optimalFissionRate.X + optimalFissionRate.Y) / 2.0f; - DebugConsole.Log($"Fission rate reset: {targetFissionRate}, {fissionRate} -> {desiredFissionRate}"); - targetFissionRate = desiredFissionRate; - fissionRate = desiredFissionRate; - float desiredTemperature = (optimalTemperature.X + optimalTemperature.Y) / 2.0f; DebugConsole.Log($"Temperature reset: {temperature} -> {desiredTemperature}"); temperature = desiredTemperature; + + float desiredFissionRate = GetFissionRateForTargetTemperatureAndTurbineOutput(desiredTemperature, desiredTurbineOutput); + DebugConsole.Log($"Fission rate reset: {targetFissionRate}, {fissionRate} -> {desiredFissionRate}"); + targetFissionRate = desiredFissionRate; + fissionRate = desiredFissionRate; } loadQueue.Enqueue(currentLoad); @@ -420,6 +425,12 @@ namespace Barotrauma.Items.Components return fissionRate * (prevAvailableFuel / 100.0f) * 2.0f; } + private float GetFissionRateForTargetTemperatureAndTurbineOutput(float temperature, float turbineOutput) + { + if (MathUtils.NearlyEqual(AvailableFuel, 0f)) { return 0f; } + return (temperature + turbineOutput) / (AvailableFuel / 100f) / 2f; + } + /// /// Do we need more fuel to generate enough power to match the current load. /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index 086924fd5..104d8cb08 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -44,6 +44,8 @@ namespace Barotrauma.Items.Components private readonly Queue impactQueue = new Queue(); + private bool removePending; + //continuous collision detection is used while the projectile is moving faster than this const float ContinuousCollisionThreshold = 5.0f; @@ -274,10 +276,9 @@ namespace Barotrauma.Items.Components { if (character != null && !characterUsable) { return false; } - for (int i = 0; i < HitScanCount; i++) { - float launchAngle = 0f; + float launchAngle; if (StaticSpread) { @@ -305,6 +306,7 @@ namespace Barotrauma.Items.Components } else { + item.body.SetTransform(item.body.SimPosition, launchAngle); float modifiedLaunchImpulse = LaunchImpulse * (1 + Rand.Range(-ImpulseSpread, ImpulseSpread)); DoLaunch(launchDir * modifiedLaunchImpulse * item.body.Mass); } @@ -562,14 +564,17 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { - ApplyStatusEffects(ActionType.OnActive, deltaTime, null); - while (impactQueue.Count > 0) { var impact = impactQueue.Dequeue(); HandleProjectileCollision(impact.Fixture, impact.Normal, impact.LinearVelocity); } + if (!removePending) + { + ApplyStatusEffects(ActionType.OnActive, deltaTime, null); + } + if (item.body != null && item.body.FarseerBody.IsBullet) { if (item.body.LinearVelocity.LengthSquared() < ContinuousCollisionThreshold * ContinuousCollisionThreshold) @@ -668,6 +673,10 @@ namespace Barotrauma.Items.Components hits.Add(target.Body); impactQueue.Enqueue(new Impact(target, contact.Manifold.LocalNormal, item.body.LinearVelocity)); IsActive = true; + if (RemoveOnHit) + { + item.body.FarseerBody.ResetDynamics(); + } if (hits.Count() >= MaxTargetsToHit || target.Body.UserData is VoronoiCell) { Deactivate(); @@ -867,15 +876,10 @@ namespace Barotrauma.Items.Components if (RemoveOnHit) { - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) - { - //clients aren't allowed to remove items by themselves, so lets hide the projectile until the server tells us to remove it - item.HiddenInGame = Hitscan; - } - else - { - Entity.Spawner?.AddToRemoveQueue(item); - } + removePending = true; + item.HiddenInGame = true; + item.body.FarseerBody.Enabled = false; + Entity.Spawner?.AddToRemoveQueue(item); } return true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs index a97527ea6..bc7f71527 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs @@ -95,7 +95,7 @@ namespace Barotrauma.Items.Components } private string[] labels; - [Serialize("", true, description: "The texts displayed on the buttons/tickboxes, separated by commas.")] + [Serialize("", true, description: "The texts displayed on the buttons/tickboxes, separated by commas.", alwaysUseInstanceValues: true)] public string Labels { get { return string.Join(",", labels); } @@ -111,7 +111,7 @@ namespace Barotrauma.Items.Components } private string[] signals; - [Serialize("", true, description: "The signals sent when the buttons are pressed or the tickboxes checked, separated by commas.")] + [Serialize("", true, description: "The signals sent when the buttons are pressed or the tickboxes checked, separated by commas.", alwaysUseInstanceValues: true)] public string Signals { //use semicolon as a separator because comma may be needed in the signals (for color or vector values for example) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs index 0c292771d..a836fab59 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs @@ -20,9 +20,10 @@ namespace Barotrauma.Items.Components private bool castShadows; private bool drawBehindSubs; - private double lastToggleSignalTime; + private string prevColorSignal; + public PhysicsBody ParentBody; private Turret turret; @@ -326,7 +327,11 @@ namespace Barotrauma.Items.Components IsOn = signal.value != "0"; break; case "set_color": - LightColor = XMLExtensions.ParseColor(signal.value, false); + if (signal.value != prevColorSignal) + { + LightColor = XMLExtensions.ParseColor(signal.value, false); + prevColorSignal = signal.value; + } break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs index 5eee68c50..f8da64793 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs @@ -164,6 +164,7 @@ namespace Barotrauma.Items.Components if (Math.Abs(item.body.LinearVelocity.X) > MinimumVelocity || Math.Abs(item.body.LinearVelocity.Y) > MinimumVelocity) { MotionDetected = true; + return; } } @@ -173,67 +174,93 @@ namespace Barotrauma.Items.Components float broadRangeY = Math.Max(rangeY * 2, 500); if (item.CurrentHull == null && item.Submarine != null && Level.Loaded != null && - (Target == TargetType.Wall || Target == TargetType.Any) && - (Math.Abs(item.Submarine.Velocity.X) > MinimumVelocity || Math.Abs(item.Submarine.Velocity.Y) > MinimumVelocity)) + (Target == TargetType.Wall || Target == TargetType.Any)) { - var cells = Level.Loaded.GetCells(item.WorldPosition, 1); - foreach (var cell in cells) + if (Math.Abs(item.Submarine.Velocity.X) > MinimumVelocity || Math.Abs(item.Submarine.Velocity.Y) > MinimumVelocity) { - if (cell.IsPointInside(item.WorldPosition)) + var cells = Level.Loaded.GetCells(item.WorldPosition, 1); + foreach (var cell in cells) { - MotionDetected = true; - return; - } - foreach (var edge in cell.Edges) - { - var closestPoint = MathUtils.GetClosestPointOnLineSegment(edge.Point1 + cell.Translation, edge.Point2 + cell.Translation, item.WorldPosition); - if (Math.Abs(closestPoint.X - item.WorldPosition.X) < rangeX && Math.Abs(closestPoint.Y - item.WorldPosition.Y) < rangeY) + if (cell.IsPointInside(item.WorldPosition)) { MotionDetected = true; return; } - } + foreach (var edge in cell.Edges) + { + Vector2 e1 = edge.Point1 + cell.Translation; + Vector2 e2 = edge.Point2 + cell.Translation; + if (MathUtils.LinesIntersect(e1, e2, new Vector2(detectRect.X, detectRect.Y), new Vector2(detectRect.Right, detectRect.Y)) || + MathUtils.LinesIntersect(e1, e2, new Vector2(detectRect.X, detectRect.Bottom), new Vector2(detectRect.Right, detectRect.Bottom)) || + MathUtils.LinesIntersect(e1, e2, new Vector2(detectRect.X, detectRect.Y), new Vector2(detectRect.X, detectRect.Bottom)) || + MathUtils.LinesIntersect(e1, e2, new Vector2(detectRect.Right, detectRect.Y), new Vector2(detectRect.Right, detectRect.Bottom))) + { + MotionDetected = true; + return; + } + } + } } - } - - foreach (Character c in Character.CharacterList) - { - if (IgnoreDead && c.IsDead) { continue; } - - //ignore characters that have spawned a second or less ago - //makes it possible to detect when a spawned character moves without triggering the detector immediately as the ragdoll spawns and drops to the ground - if (c.SpawnTime > Timing.TotalTime - 1.0) { continue; } - - switch (Target) + foreach (Submarine sub in Submarine.Loaded) { - case TargetType.Human: - if (!c.IsHuman) { continue; } - break; - case TargetType.Monster: - if (c.IsHuman || c.IsPet) { continue; } - break; - case TargetType.Wall: - break; - } + if (sub == item.Submarine) { continue; } - //do a rough check based on the position of the character's collider first - //before the more accurate limb-based check - if (Math.Abs(c.WorldPosition.X - detectPos.X) > broadRangeX || Math.Abs(c.WorldPosition.Y - detectPos.Y) > broadRangeY) - { - continue; - } + Vector2 relativeVelocity = item.Submarine.Velocity - sub.Velocity; + if (Math.Abs(relativeVelocity.X) < MinimumVelocity && Math.Abs(relativeVelocity.Y) < MinimumVelocity) { continue; } - foreach (Limb limb in c.AnimController.Limbs) - { - if (limb.IsSevered) { continue; } - if (limb.LinearVelocity.LengthSquared() <= MinimumVelocity * MinimumVelocity) { continue; } - if (MathUtils.CircleIntersectsRectangle(limb.WorldPosition, ConvertUnits.ToDisplayUnits(limb.body.GetMaxExtent()), detectRect)) + Rectangle worldBorders = new Rectangle( + sub.Borders.X + (int)sub.WorldPosition.X, + sub.Borders.Y + (int)sub.WorldPosition.Y - sub.Borders.Height, + sub.Borders.Width, + sub.Borders.Height); + + if (worldBorders.Intersects(detectRect)) { MotionDetected = true; return; } } } + + if (Target != TargetType.Wall) + { + foreach (Character c in Character.CharacterList) + { + if (IgnoreDead && c.IsDead) { continue; } + + //ignore characters that have spawned a second or less ago + //makes it possible to detect when a spawned character moves without triggering the detector immediately as the ragdoll spawns and drops to the ground + if (c.SpawnTime > Timing.TotalTime - 1.0) { continue; } + + switch (Target) + { + case TargetType.Human: + if (!c.IsHuman) { continue; } + break; + case TargetType.Monster: + if (c.IsHuman || c.IsPet) { continue; } + break; + } + + //do a rough check based on the position of the character's collider first + //before the more accurate limb-based check + if (Math.Abs(c.WorldPosition.X - detectPos.X) > broadRangeX || Math.Abs(c.WorldPosition.Y - detectPos.Y) > broadRangeY) + { + continue; + } + + foreach (Limb limb in c.AnimController.Limbs) + { + if (limb.IsSevered) { continue; } + if (limb.LinearVelocity.LengthSquared() <= MinimumVelocity * MinimumVelocity) { continue; } + if (MathUtils.CircleIntersectsRectangle(limb.WorldPosition, ConvertUnits.ToDisplayUnits(limb.body.GetMaxExtent()), detectRect)) + { + MotionDetected = true; + return; + } + } + } + } } public override void FlipX(bool relativeToSub) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index 4d369329a..076aff11c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -15,8 +15,8 @@ namespace Barotrauma.Items.Components partial class Turret : Powered, IDrawableComponent, IServerSerializable { private Sprite barrelSprite, railSprite; - private List> chargeSprites = new List>(); - private List spinningBarrelSprites = new List(); + private readonly List<(Sprite sprite, Vector2 position)> chargeSprites = new List<(Sprite sprite, Vector2 position)>(); + private readonly List spinningBarrelSprites = new List(); private Vector2 barrelPos; private Vector2 transformedBarrelPos; @@ -290,7 +290,7 @@ namespace Barotrauma.Items.Components railSprite = new Sprite(subElement); break; case "chargesprite": - chargeSprites.Add(new Tuple(new Sprite(subElement), subElement.GetAttributeVector2("chargetarget", Vector2.Zero))); + chargeSprites.Add((new Sprite(subElement), subElement.GetAttributeVector2("chargetarget", Vector2.Zero))); break; case "spinningbarrelsprite": int spriteCount = subElement.GetAttributeInt("spriteamount", 1); @@ -1283,7 +1283,6 @@ namespace Barotrauma.Items.Components private Vector2 GetRelativeFiringPosition(bool useOffset = true) { - // i don't feel great about this method, should be evaluated again Vector2 transformedFiringOffset = Vector2.Zero; if (useOffset) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs index 0bb2e3652..b541c4812 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs @@ -202,7 +202,7 @@ namespace Barotrauma namespace Barotrauma.Items.Components { - class Wearable : Pickable, IServerSerializable + partial class Wearable : Pickable, IServerSerializable { private readonly XElement[] wearableElements; private readonly WearableSprite[] wearableSprites; @@ -210,6 +210,7 @@ namespace Barotrauma.Items.Components private readonly Limb[] limb; private readonly List damageModifiers; + public readonly Dictionary SkillModifiers; public IEnumerable DamageModifiers { @@ -265,7 +266,8 @@ namespace Barotrauma.Items.Components this.item = item; damageModifiers = new List(); - + SkillModifiers = new Dictionary(); + int spriteCount = element.Elements().Count(x => x.Name.ToString() == "sprite"); Variants = element.GetAttributeInt("variants", 0); variant = Rand.Range(1, Variants + 1, Rand.RandSync.Server); @@ -308,6 +310,18 @@ namespace Barotrauma.Items.Components case "damagemodifier": damageModifiers.Add(new DamageModifier(subElement, item.Name + ", Wearable")); break; + case "skillmodifier": + string skillIdentifier = subElement.GetAttributeString("skillidentifier", string.Empty); + float skillValue = subElement.GetAttributeFloat("skillvalue", 0f); + if (SkillModifiers.ContainsKey(skillIdentifier)) + { + SkillModifiers[skillIdentifier] += skillValue; + } + else + { + SkillModifiers.TryAdd(skillIdentifier, skillValue); + } + break; } } } @@ -324,7 +338,7 @@ namespace Barotrauma.Items.Components { var wearableSprite = wearableSprites[i]; if (!wearableSprite.IsInitialized) { wearableSprite.Init(picker.Info?.Gender ?? Gender.None); } - if (picker.Info?.Gender != Gender.None && (wearableSprite.Gender != Gender.None)) + if (picker.Info != null && picker.Info?.Gender != Gender.None && (wearableSprite.Gender != Gender.None)) { // If the item is gender specific (it has a different textures for male and female), we have to change the gender here so that the texture is updated. wearableSprite.Gender = picker.Info.Gender; @@ -386,6 +400,7 @@ namespace Barotrauma.Items.Components { if (character == null || character.Removed) { return; } if (picker == null) { return; } + for (int i = 0; i < wearableSprites.Length; i++) { Limb equipLimb = character.AnimController.GetLimb(limbType[i]); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 4954907d3..0cb65f4e2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -204,6 +204,13 @@ namespace Barotrauma set; } + [Serialize(false, true)] + public bool PurchasedNewSwap + { + get; + set; + } + /// /// Checks both and /// @@ -705,7 +712,7 @@ namespace Barotrauma get { return allPropertyObjects; } } - public bool IgnoreByAI => OrderedToBeIgnored || HasTag("ignorebyai"); + public bool IgnoreByAI(Character character) => HasTag("ignorebyai") || OrderedToBeIgnored && character.IsOnPlayerTeam; public bool OrderedToBeIgnored { get; set; } public Item(ItemPrefab itemPrefab, Vector2 position, Submarine submarine, ushort id = Entity.NullEntityID) @@ -1280,16 +1287,16 @@ namespace Barotrauma /// /// Should this item or any of its containers be ignored by the AI? /// - public bool IsThisOrAnyContainerIgnoredByAI() + public bool IsThisOrAnyContainerIgnoredByAI(Character character) { - if (IgnoreByAI) { return true; } + if (IgnoreByAI(character)) { return true; } if (Container == null) { return false; } - if (Container.IgnoreByAI) { return true; } + if (Container.IgnoreByAI(character)) { return true; } var container = Container; while (container.Container != null) { container = container.Container; - if (container.IgnoreByAI) { return true; } + if (container.IgnoreByAI(character)) { return true; } } return false; } @@ -2779,6 +2786,7 @@ namespace Barotrauma item.SpriteDepth = element.GetAttributeFloat("spritedepth", item.SpriteDepth); item.SpriteColor = element.GetAttributeColor("spritecolor", item.SpriteColor); item.Rotation = element.GetAttributeFloat("rotation", item.Rotation); + item.PurchasedNewSwap = element.GetAttributeBool("purchasednewswap", false); float scaleRelativeToPrefab = element.GetAttributeFloat(item.scale, "scale", "Scale") / oldPrefab.Scale; item.Scale *= scaleRelativeToPrefab; @@ -2787,16 +2795,41 @@ namespace Barotrauma { Vector2 oldRelativeOrigin = (oldPrefab.SwappableItem.SwapOrigin - oldPrefab.Size / 2) * element.GetAttributeFloat(item.scale, "scale", "Scale"); oldRelativeOrigin.Y = -oldRelativeOrigin.Y; - oldRelativeOrigin = MathUtils.RotatePoint(oldRelativeOrigin, item.rotationRad); + oldRelativeOrigin = MathUtils.RotatePoint(oldRelativeOrigin, -item.rotationRad); Vector2 oldOrigin = centerPos + oldRelativeOrigin; Vector2 relativeOrigin = (prefab.SwappableItem.SwapOrigin - prefab.Size / 2) * item.Scale; relativeOrigin.Y = -relativeOrigin.Y; - relativeOrigin = MathUtils.RotatePoint(relativeOrigin, item.rotationRad); + relativeOrigin = MathUtils.RotatePoint(relativeOrigin, -item.rotationRad); Vector2 origin = new Vector2(rect.X + rect.Width / 2, rect.Y - rect.Height / 2) + relativeOrigin; item.rect.Location -= (origin - oldOrigin).ToPoint(); } + + if (item.PurchasedNewSwap && !string.IsNullOrEmpty(appliedSwap.SwappableItem?.SpawnWithId)) + { + var container = item.GetComponent(); + if (container != null) + { + container.SpawnWithId = appliedSwap.SwappableItem.SpawnWithId; + } + /*string[] splitIdentifier = appliedSwap.SwappableItem.SpawnWithId.Split(','); + foreach (string id in splitIdentifier) + { + ItemPrefab itemToSpawn = ItemPrefab.Find(name: null, identifier: id.Trim()); + if (itemToSpawn == null) + { + DebugConsole.ThrowError($"Failed to spawn an item inside the purchased {item.Name} (could not find an item with the identifier \"{id}\")."); + } + else + { + var spawnedItem = new Item(itemToSpawn, Vector2.Zero, null); + item.OwnInventory.TryPutItem(spawnedItem, null, spawnedItem.AllowedSlots, createNetworkEvent: false); + Spawner?.AddToSpawnQueue(itemToSpawn, item.OwnInventory, spawnIfInventoryFull: false); + } + }*/ + } + item.PurchasedNewSwap = false; } float condition = element.GetAttributeFloat("condition", item.MaxCondition); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index f86000589..d2a4ce88b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -210,6 +210,10 @@ namespace Barotrauma public readonly string ReplacementOnUninstall; + public string SpawnWithId; + + public string SwapIdentifier; + public readonly Vector2 SwapOrigin; public List<(string requiredTag, string swapTo)> ConnectedItemsToSwap = new List<(string requiredTag, string swapTo)>(); @@ -225,9 +229,11 @@ namespace Barotrauma public SwappableItem(XElement element) { BasePrice = Math.Max(element.GetAttributeInt("price", 0), 0); + SwapIdentifier = element.GetAttributeString("swapidentifier", string.Empty); CanBeBought = element.GetAttributeBool("canbebought", BasePrice != 0); ReplacementOnUninstall = element.GetAttributeString("replacementonuninstall", ""); SwapOrigin = element.GetAttributeVector2("origin", Vector2.One); + SpawnWithId = element.GetAttributeString("spawnwithid", string.Empty); foreach (XElement subElement in element.Elements()) { @@ -357,6 +363,14 @@ namespace Barotrauma private set; } + //if true then players can only highlight the item if its targeted for interaction by a campaign event + [Serialize(false, false)] + public bool RequireCampaignInteract + { + get; + private set; + } + //should the camera focus on the item when selected [Serialize(false, false)] public bool FocusOnSelected @@ -730,6 +744,9 @@ namespace Barotrauma //nameidentifier can be used to make multiple items use the same names and descriptions string nameIdentifier = element.GetAttributeString("nameidentifier", ""); + //only used if the item doesn't have a name/description defined in the currently selected language + string fallbackNameIdentifier = element.GetAttributeString("fallbacknameidentifier", ""); + //works the same as nameIdentifier, but just replaces the description string descriptionIdentifier = element.GetAttributeString("descriptionidentifier", ""); @@ -737,11 +754,11 @@ namespace Barotrauma { if (string.IsNullOrEmpty(nameIdentifier)) { - name = TextManager.Get("EntityName." + identifier, true) ?? string.Empty; + name = TextManager.Get("EntityName." + identifier, true, "EntityName." + fallbackNameIdentifier) ?? string.Empty; } else { - name = TextManager.Get("EntityName." + nameIdentifier, true) ?? string.Empty; + name = TextManager.Get("EntityName." + nameIdentifier, true, "EntityName." + fallbackNameIdentifier) ?? string.Empty; } } else if (Category.HasFlag(MapEntityCategory.Legacy)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index 7536b319d..53c8a8a8f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -130,7 +130,7 @@ namespace Barotrauma float displayRange = Attack.Range; - Vector2 cameraPos = Character.Controlled != null ? Character.Controlled.WorldPosition : GameMain.GameScreen.Cam.Position; + 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); #if CLIENT diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/ISpatialEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/ISpatialEntity.cs index 5f1b94581..bbaad892e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/ISpatialEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/ISpatialEntity.cs @@ -12,7 +12,7 @@ namespace Barotrauma interface IIgnorable : ISpatialEntity { - bool IgnoreByAI { get; } + bool IgnoreByAI(Character character); bool OrderedToBeIgnored { get; set; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs index 8ce97ea4d..21f78b9af 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs @@ -36,7 +36,7 @@ namespace Barotrauma public Rectangle Bounds; - public ItemAssemblyPrefab(string filePath) + public ItemAssemblyPrefab(string filePath, bool allowOverwrite = false) { FilePath = filePath; XDocument doc = XMLExtensions.TryLoadXml(filePath); @@ -113,6 +113,10 @@ namespace Barotrauma new Rectangle(0, 0, 1, 1) : new Rectangle(minX, minY, maxX - minX, maxY - minY); + if (allowOverwrite && Prefabs.ContainsKey(identifier)) + { + Prefabs.Remove(Prefabs[identifier]); + } Prefabs.Add(this, doc.Root.IsOverride()); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 7d82128c1..382c0a8ab 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -1894,26 +1894,45 @@ namespace Barotrauma private void CalculateTunnelDistanceField(int density) { distanceField = new List<(Point point, double distance)>(); - for (int x = 0; x < Size.X; x += density) + + if (Mirrored) { - for (int y = 0; y < Size.Y; y += density) + for (int x = Size.X - 1; x >= 0; x -= density) { - Point point = new Point(x, y); - double shortestDistSqr = double.PositiveInfinity; - foreach (Tunnel tunnel in Tunnels) + for (int y = 0; y < Size.Y; y += density) { - for (int i = 1; i < tunnel.Nodes.Count; i++) - { - shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.LineSegmentToPointDistanceSquared(tunnel.Nodes[i - 1], tunnel.Nodes[i], point)); - } + addPoint(x, y); } - shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.DistanceSquared((double)point.X, (double)point.Y, (double)startPosition.X, (double)startPosition.Y)); - shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.DistanceSquared((double)point.X, (double)point.Y, (double)startExitPosition.X, (double)borders.Bottom)); - shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.DistanceSquared((double)point.X, (double)point.Y, (double)endPosition.X, (double)endPosition.Y)); - shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.DistanceSquared((double)point.X, (double)point.Y, (double)endExitPosition.X, (double)borders.Bottom)); - distanceField.Add((point, Math.Sqrt(shortestDistSqr))); } } + else + { + for (int x = 0; x < Size.X; x += density) + { + for (int y = 0; y < Size.Y; y += density) + { + addPoint(x, y); + } + } + } + + void addPoint(int x, int y) + { + Point point = new Point(x, y); + double shortestDistSqr = double.PositiveInfinity; + foreach (Tunnel tunnel in Tunnels) + { + for (int i = 1; i < tunnel.Nodes.Count; i++) + { + shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.LineSegmentToPointDistanceSquared(tunnel.Nodes[i - 1], tunnel.Nodes[i], point)); + } + } + shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.DistanceSquared((double)point.X, (double)point.Y, (double)startPosition.X, (double)startPosition.Y)); + shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.DistanceSquared((double)point.X, (double)point.Y, (double)startExitPosition.X, (double)borders.Bottom)); + shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.DistanceSquared((double)point.X, (double)point.Y, (double)endPosition.X, (double)endPosition.Y)); + shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.DistanceSquared((double)point.X, (double)point.Y, (double)endExitPosition.X, (double)borders.Bottom)); + distanceField.Add((point, Math.Sqrt(shortestDistSqr))); + } } private double GetDistToTunnel(Vector2 position, Tunnel tunnel) @@ -3554,7 +3573,7 @@ namespace Barotrauma { spawnPos.Y = Math.Min(Size.Y - outpost.Borders.Height * 0.6f, spawnPos.Y + outpost.Borders.Height / 2); } - outpost.SetPosition(spawnPos); + outpost.SetPosition(spawnPos, forceUndockFromStaticSubmarines: false); if ((i == 0) == !Mirrored) { StartOutpost = outpost; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs index 62e9e02ca..afc15291e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs @@ -354,7 +354,7 @@ namespace Barotrauma } } - sub.SetPosition(sub.WorldPosition - Submarine.WorldPosition); + sub.SetPosition(sub.WorldPosition - Submarine.WorldPosition, forceUndockFromStaticSubmarines: false); sub.Submarine = Submarine; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index 7ddec0380..35120ad55 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -13,14 +13,14 @@ namespace Barotrauma public class TakenItem { public readonly ushort OriginalID; - public readonly ushort OriginalContainerID; public readonly ushort ModuleIndex; public readonly string Identifier; + public readonly int OriginalContainerIndex; - public TakenItem(string identifier, UInt16 originalID, UInt16 originalContainerID, ushort moduleIndex) + public TakenItem(string identifier, UInt16 originalID, UInt16 originalContainerIndex, ushort moduleIndex) { OriginalID = originalID; - OriginalContainerID = originalContainerID; + OriginalContainerIndex = originalContainerIndex; ModuleIndex = moduleIndex; Identifier = identifier; } @@ -29,11 +29,7 @@ namespace Barotrauma { System.Diagnostics.Debug.Assert(item.OriginalModuleIndex >= 0, "Trying to add a non-outpost item to a location's taken items"); - if (item.OriginalContainerID != Entity.NullEntityID) - { - OriginalContainerID = item.OriginalContainerID; - } - + OriginalContainerIndex = item.OriginalContainerIndex; OriginalID = item.ID; ModuleIndex = (ushort) item.OriginalModuleIndex; Identifier = item.prefab.Identifier; @@ -41,14 +37,14 @@ namespace Barotrauma public bool IsEqual(TakenItem obj) { - return obj.OriginalID == OriginalID && obj.OriginalContainerID == OriginalContainerID && obj.ModuleIndex == ModuleIndex && obj.Identifier == Identifier; + return obj.OriginalID == OriginalID && obj.OriginalContainerIndex == OriginalContainerIndex && obj.ModuleIndex == ModuleIndex && obj.Identifier == Identifier; } public bool Matches(Item item) { - if (item.OriginalContainerID != Entity.NullEntityID) + if (item.OriginalContainerIndex != Entity.NullEntityID) { - return item.OriginalContainerID == OriginalContainerID && item.OriginalModuleIndex == ModuleIndex && item.prefab.Identifier == Identifier; + return item.OriginalContainerIndex == OriginalContainerIndex && item.OriginalModuleIndex == ModuleIndex && item.prefab.Identifier == Identifier; } else { @@ -184,7 +180,11 @@ namespace Barotrauma private readonly List selectedMissions = new List(); public IEnumerable SelectedMissions { - get { return selectedMissions; } + get + { + selectedMissions.RemoveAll(m => !availableMissions.Contains(m)); + return selectedMissions; + } } public void SelectMission(Mission mission) @@ -345,9 +345,9 @@ namespace Barotrauma DebugConsole.ThrowError($"Error in saved location: could not parse taken item id \"{takenItemSplit[1]}\""); continue; } - if (!ushort.TryParse(takenItemSplit[2], out ushort containerId)) + if (!ushort.TryParse(takenItemSplit[2], out ushort containerIndex)) { - DebugConsole.ThrowError($"Error in saved location: could not parse taken container id \"{takenItemSplit[2]}\""); + DebugConsole.ThrowError($"Error in saved location: could not parse taken container index \"{takenItemSplit[2]}\""); continue; } if (!ushort.TryParse(takenItemSplit[3], out ushort moduleIndex)) @@ -355,7 +355,7 @@ namespace Barotrauma DebugConsole.ThrowError($"Error in saved location: could not parse taken item module index \"{takenItemSplit[3]}\""); continue; } - takenItems.Add(new TakenItem(takenItemSplit[0], id, containerId, moduleIndex)); + takenItems.Add(new TakenItem(takenItemSplit[0], id, containerIndex, moduleIndex)); } killedCharacterIdentifiers = element.GetAttributeIntArray("killedcharacters", new int[0]).ToHashSet(); @@ -1153,7 +1153,7 @@ namespace Barotrauma { locationElement.Add(new XAttribute( "takenitems", - string.Join(',', takenItems.Select(it => it.Identifier + ";" + it.OriginalID + ";" + it.OriginalContainerID + ";" + it.ModuleIndex)))); + string.Join(',', takenItems.Select(it => it.Identifier + ";" + it.OriginalID + ";" + it.OriginalContainerIndex + ";" + it.ModuleIndex)))); } if (killedCharacterIdentifiers.Any()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs index e9517fff8..61e14591b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -243,7 +243,7 @@ namespace Barotrauma /// public int OriginalModuleIndex = -1; - public UInt16 OriginalContainerID; + public int OriginalContainerIndex = -1; public virtual string Name { @@ -509,9 +509,9 @@ namespace Barotrauma mapEntityList.Remove(this); #if CLIENT - if (selectedList.Contains(this)) + if (SelectedList.Contains(this)) { - selectedList = selectedList.FindAll(e => e != this); + SelectedList = SelectedList.Where(e => e != this).ToHashSet(); } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index 9980cf459..2e07a4c11 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs @@ -186,7 +186,8 @@ namespace Barotrauma foreach (Hull hull in Hull.hullList) { if (hull.Submarine != sub) { continue; } - if (hull.RoomName.Contains("RoomName.", StringComparison.OrdinalIgnoreCase)) + if (string.IsNullOrEmpty(hull.RoomName) || + hull.RoomName.Contains("RoomName.", StringComparison.OrdinalIgnoreCase)) { hull.RoomName = hull.CreateRoomName(); } @@ -1436,6 +1437,7 @@ namespace Barotrauma var npc = Character.Create(CharacterPrefab.HumanConfigFile, SpawnAction.OffsetSpawnPos(gotoTarget.WorldPosition, 100.0f), ToolBox.RandomSeed(8), characterInfo, hasAi: true, createNetworkEvent: true); npc.AnimController.FindHull(gotoTarget.WorldPosition, true); npc.TeamID = CharacterTeamType.FriendlyNPC; + npc.Prefab = humanPrefab; if (!outpost.Info.OutpostNPCs.ContainsKey(humanPrefab.Identifier)) { outpost.Info.OutpostNPCs.Add(humanPrefab.Identifier, new List()); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index 6116a8eeb..e83ae7dbf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -27,7 +27,7 @@ namespace Barotrauma public Submarine Submarine => Wall.Submarine; public Rectangle WorldRect => Submarine == null ? rect : new Rectangle((int)(rect.X + Submarine.Position.X), (int)(rect.Y + Submarine.Position.Y), rect.Width, rect.Height); - public bool IgnoreByAI => OrderedToBeIgnored; + public bool IgnoreByAI(Character character) => OrderedToBeIgnored && character.IsOnPlayerTeam; public bool OrderedToBeIgnored { get; set; } public WallSection(Rectangle rect, Structure wall, float damage = 0.0f) @@ -872,13 +872,17 @@ namespace Barotrauma public Vector2 SectionPosition(int sectionIndex, bool world = false) { - if (sectionIndex < 0 || sectionIndex >= Sections.Length) return Vector2.Zero; + if (sectionIndex < 0 || sectionIndex >= Sections.Length) + { + return Vector2.Zero; + } if (Prefab.BodyRotation == 0.0f) { Vector2 sectionPos = new Vector2( Sections[sectionIndex].rect.X + Sections[sectionIndex].rect.Width / 2.0f, Sections[sectionIndex].rect.Y - Sections[sectionIndex].rect.Height / 2.0f); + if (world && Submarine != null) { sectionPos += Submarine.Position; @@ -897,8 +901,11 @@ namespace Barotrauma { diffFromCenter = ((sectionRect.Y - sectionRect.Height / 2) - (rect.Y - rect.Height / 2)) / (float)rect.Height * BodyHeight; } - if (FlippedX) diffFromCenter = -diffFromCenter; - + if (FlippedX) + { + diffFromCenter = -diffFromCenter; + } + Vector2 sectionPos = Position + new Vector2( (float)Math.Cos(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation), (float)Math.Sin(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation)) * diffFromCenter; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs index d4e620846..380df3975 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs @@ -250,17 +250,21 @@ namespace Barotrauma var parentType = element.Parent?.GetAttributeString("prefabtype", "") ?? string.Empty; string nameIdentifier = element.GetAttributeString("nameidentifier", ""); + + //only used if the item doesn't have a name/description defined in the currently selected language + string fallbackNameIdentifier = element.GetAttributeString("fallbacknameidentifier", ""); + string descriptionIdentifier = element.GetAttributeString("descriptionidentifier", ""); if (string.IsNullOrEmpty(sp.originalName)) { if (string.IsNullOrEmpty(nameIdentifier)) { - sp.name = TextManager.Get("EntityName." + sp.identifier, true) ?? string.Empty; + sp.name = TextManager.Get("EntityName." + sp.identifier, true, "EntityName." + fallbackNameIdentifier) ?? string.Empty; } else { - sp.name = TextManager.Get("EntityName." + nameIdentifier, true) ?? string.Empty; + sp.name = TextManager.Get("EntityName." + nameIdentifier, true, "EntityName." + fallbackNameIdentifier) ?? string.Empty; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index f31ae6f5c..3ac28ec97 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -1174,7 +1174,7 @@ namespace Barotrauma prevPosition = position; } - public void SetPosition(Vector2 position, List checkd = null) + public void SetPosition(Vector2 position, List checkd = null, bool forceUndockFromStaticSubmarines = true) { if (!MathUtils.IsValid(position)) { return; } @@ -1188,7 +1188,7 @@ namespace Barotrauma foreach (Submarine dockedSub in DockedTo) { - if (dockedSub.PhysicsBody.BodyType == BodyType.Static) + if (dockedSub.PhysicsBody.BodyType == BodyType.Static && forceUndockFromStaticSubmarines) { if (ConnectedDockingPorts.TryGetValue(dockedSub, out DockingPort port)) { @@ -1198,7 +1198,7 @@ namespace Barotrauma } Vector2? expectedLocation = CalculateDockOffset(this, dockedSub); if (expectedLocation == null) { continue; } - dockedSub.SetPosition(position + expectedLocation.Value, checkd); + dockedSub.SetPosition(position + expectedLocation.Value, checkd, forceUndockFromStaticSubmarines); dockedSub.UpdateTransform(interpolate: false); } } @@ -1610,6 +1610,7 @@ namespace Barotrauma } foreach (Item itemToSwap in itemsToSwap) { + itemToSwap.PurchasedNewSwap = item.PurchasedNewSwap; if (itemPrefab != itemToSwap.Prefab) { itemToSwap.PendingItemSwap = itemPrefab; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs index 796e4f0f5..4c94d8128 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs @@ -322,19 +322,30 @@ namespace Barotrauma Math.Max(Body.LinearVelocity.Y, ConvertUnits.ToSimUnits(Level.Loaded.BottomPos - (worldBorders.Y - worldBorders.Height)))); } - if (Position.X < 0) + //hard limit for how far outside the level the sub can go + float maxDist = 200000.0f; + //the force of the current starts to increase exponentially after this point + float exponentialForceIncreaseDist = 150000.0f; + float distance = Position.X < 0 ? Math.Abs(Position.X) : Position.X - Level.Loaded.Size.X; + if (distance > 0) { - float force = Math.Abs(Position.X * 0.5f); - totalForce += Vector2.UnitX * force; - if (Character.Controlled != null && Character.Controlled.Submarine == submarine) + if (distance > maxDist) { - GameMain.GameScreen.Cam.Shake = Math.Max(GameMain.GameScreen.Cam.Shake, Math.Min(force * 0.0001f, 5.0f)); + if (Position.X < 0) + { + Body.LinearVelocity = new Vector2(Math.Max(0, Body.LinearVelocity.X), Body.LinearVelocity.Y); + } + else + { + Body.LinearVelocity = new Vector2(Math.Min(0, Body.LinearVelocity.X), Body.LinearVelocity.Y); + } } - } - else - { - float force = (Position.X - Level.Loaded.Size.X) * 0.5f; - totalForce -= Vector2.UnitX * force; + if (distance > exponentialForceIncreaseDist) + { + distance += (float)Math.Pow((distance - exponentialForceIncreaseDist) * 0.01f, 2.0f); + } + float force = distance * 0.5f; + totalForce += (Position.X < 0 ? Vector2.UnitX : -Vector2.UnitX) * force; if (Character.Controlled != null && Character.Controlled.Submarine == submarine) { GameMain.GameScreen.Cam.Shake = Math.Max(GameMain.GameScreen.Cam.Shake, Math.Min(force * 0.0001f, 5.0f)); @@ -828,9 +839,9 @@ namespace Barotrauma } #if CLIENT - if (Character.Controlled != null && Character.Controlled.Submarine == submarine) + if (Character.Controlled != null && Character.Controlled.Submarine == submarine && Character.Controlled.KnockbackCooldownTimer <= 0.0f) { - GameMain.GameScreen.Cam.Shake = impact * 10.0f; + GameMain.GameScreen.Cam.Shake = Math.Max(impact * 10.0f, GameMain.GameScreen.Cam.Shake); if (submarine.Info.Type == SubmarineType.Player && !submarine.DockedTo.Any(s => s.Info.Type != SubmarineType.Player)) { float angularVelocity = diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs index 33a2d6481..8e6dc5f07 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs @@ -535,7 +535,6 @@ namespace Barotrauma XDocument doc = new XDocument(newElement); doc.Root.Add(new XAttribute("name", Name)); - if (previewImage != null) { doc.Root.Add(new XAttribute("previewimage", Convert.ToBase64String(previewImage.ToArray()))); @@ -692,8 +691,6 @@ namespace Barotrauma } } - static readonly string TempFolder = Path.Combine("Submarine", "Temp"); - public static XDocument OpenFile(string file) { return OpenFile(file, out _); @@ -723,7 +720,7 @@ namespace Barotrauma if (extension == ".sub") { - System.IO.Stream stream = null; + System.IO.Stream stream; try { stream = SaveUtil.DecompressFiletoStream(file); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs index c82adf09c..5c0c1999a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs @@ -829,7 +829,7 @@ namespace Barotrauma { if (assignedWayPoints[i] == null) { - DebugConsole.ThrowError("Couldn't find a waypoint for " + crew[i].Name + "!"); + DebugConsole.AddWarning("Couldn't find a waypoint for " + crew[i].Name + "!"); assignedWayPoints[i] = WayPointList[0]; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs index bee897452..a82fc6111 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs @@ -274,6 +274,7 @@ namespace Barotrauma.Networking if (hull.Submarine != RespawnShuttle) { continue; } hull.OxygenPercentage = 100.0f; hull.WaterVolume = 0.0f; + hull.BallastFlora?.Kill(); } foreach (Character c in Character.CharacterList) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs index 86eababf0..059e082f4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs @@ -35,13 +35,13 @@ namespace Barotrauma } /// - /// Returns the active prefab with identifier k. + /// Returns the active prefab with the identifier. /// - /// Prefab identifier - /// Active prefab with identifier k - public T this[string k] + /// Prefab identifier + /// Active prefab with the identifier + public T this[string identifier] { - get { return prefabs[k].Last(); } + get { return prefabs[identifier].Last(); } } /// @@ -63,13 +63,13 @@ namespace Barotrauma } /// - /// Returns true if a prefab with identifier k exists, false otherwise. + /// Returns true if a prefab with the identifier exists, false otherwise. /// - /// Prefab identifier - /// Whether a prefab with identifier k exists or not - public bool ContainsKey(string k) + /// Prefab identifier + /// Whether a prefab with the identifier exists or not + public bool ContainsKey(string identifier) { - return prefabs.ContainsKey(k); + return prefabs.ContainsKey(identifier); } /// @@ -93,11 +93,10 @@ namespace Barotrauma //Handle bad overrides and duplicates if (basePrefabExists && !isOverride) { - DebugConsole.ThrowError($"Error registering \"{prefab.OriginalName}\", \"{prefab.Identifier}\" ({typeof(T).ToString()}): base already exists; try overriding\n{Environment.StackTrace}"); + DebugConsole.ThrowError($"Failed to add the prefab \"{prefab.OriginalName}\", \"{prefab.Identifier}\" ({typeof(T)}): a prefab with the same identifier already exists; try overriding\n{Environment.StackTrace}"); return; } - //Add to list if (!basePrefabExists) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs index cd5933994..0bff8f737 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs @@ -650,7 +650,7 @@ namespace Barotrauma return dictionary; } - public static void SerializeProperties(ISerializableEntity obj, XElement element, bool saveIfDefault = false) + public static void SerializeProperties(ISerializableEntity obj, XElement element, bool saveIfDefault = false, bool ignoreEditable = false) { var saveProperties = GetProperties(obj); foreach (var property in saveProperties) @@ -667,7 +667,7 @@ namespace Barotrauma foreach (var attribute in property.Attributes.OfType()) { if ((attribute.isSaveable && !attribute.defaultValue.Equals(value)) || - property.Attributes.OfType().Any()) + (!ignoreEditable && property.Attributes.OfType().Any())) { save = true; break; diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index 502dbae88..01bb1451e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -748,7 +748,18 @@ namespace Barotrauma { owner = ownerItem.ParentInventory?.Owner; } - if (owner is Item container && HasRequiredConditions(container.AllPropertyObjects, pc.ToEnumerable(), targetingContainer: true)) { return true; } + if (owner is Item container) + { + if (pc.Type == PropertyConditional.ConditionType.HasTag) + { + //if we're checking for tags, just check the Item object, not the ItemComponents + if (HasRequiredConditions((container as ISerializableEntity).ToEnumerable(), pc.ToEnumerable(), targetingContainer: true)) { return true; } + } + else + { + if (HasRequiredConditions(container.AllPropertyObjects, pc.ToEnumerable(), targetingContainer: true)) { return true; } + } + } if (owner is Character character && HasRequiredConditions(character.ToEnumerable(), pc.ToEnumerable(), targetingContainer: true)) { return true; } } else diff --git a/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub b/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub index 9e6500f26..0211065aa 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub and b/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Berilia.sub b/Barotrauma/BarotraumaShared/Submarines/Berilia.sub index fe459925d..a24f1c7c1 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Berilia.sub and b/Barotrauma/BarotraumaShared/Submarines/Berilia.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Dugong.sub b/Barotrauma/BarotraumaShared/Submarines/Dugong.sub index 09ae292f8..7bcc4f257 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Dugong.sub and b/Barotrauma/BarotraumaShared/Submarines/Dugong.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Humpback.sub b/Barotrauma/BarotraumaShared/Submarines/Humpback.sub index 0589a5be5..86a3d98ba 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Humpback.sub and b/Barotrauma/BarotraumaShared/Submarines/Humpback.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub b/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub index 563ea6cd4..93e64afe2 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub and b/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Orca.sub b/Barotrauma/BarotraumaShared/Submarines/Orca.sub index f9ef5c781..76b34a13b 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Orca.sub and b/Barotrauma/BarotraumaShared/Submarines/Orca.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/R-29.sub b/Barotrauma/BarotraumaShared/Submarines/R-29.sub index 053269bfd..a58addced 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/R-29.sub and b/Barotrauma/BarotraumaShared/Submarines/R-29.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Remora.sub b/Barotrauma/BarotraumaShared/Submarines/Remora.sub index 0d6e002ef..46cff2776 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Remora.sub and b/Barotrauma/BarotraumaShared/Submarines/Remora.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Typhon.sub b/Barotrauma/BarotraumaShared/Submarines/Typhon.sub index f9293f457..0863bbd82 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Typhon.sub and b/Barotrauma/BarotraumaShared/Submarines/Typhon.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub b/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub index 14bad2a27..024fb5f2f 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub and b/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub differ diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 33ee75e58..1443f04e3 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,4 +1,81 @@ ------------------------------------------------------------------------------------------------------- +--------------------------------------------------------------------------------------------------------- +v0.1400.2.0 (unstable) +--------------------------------------------------------------------------------------------------------- + +Changes: +- Further improvements/balancing to the pulse laser and chaingun, including sounds and improved particle effects. +- Adjusted the amount of loot in pirate subs. +- Motion detectors that are set to detect wall can also detect other submarines. +- Doors/hatches can be damaged by projectiles. +- The loaders for newly purchased weapons spawn with one free ammo box. +- Improvements to the weapon customization menu. +- Hammerhead and Golden Hammerhead: Increased health, added some protection on claws and the tail end. Slightly increase the slow swimming speed and animations. The claws don't break anymore when shot with coilgun. +- Hammerheads and Husks don't avoid gun fire anymore. Pets now avoid gun fire. + +Fixes: +- Fixed upgrades resetting if you return to an outpost after purchasing upgrades. +- Draw coilgun and railgun muzzle flashes in front of subs. +- Fixed motion detectors detecting level walls unreliably. +- Fixed characters triggering motion sensors that are only set to be triggered by walls. +- Fixed items taken from abandoned outposts sometimes reappearing when returning to the outpost. +- Fixed highlighting items sometimes breaking after dropping a metal crate or some other container whose inventory is always visible when the item is equipped. +- Fixed bots putting minerals in the closest locker instead of preferring those that have been tagged with the "mineralcontainer" tag +- Fixed bots not respecting the access restrictions when choosing a where to put items into. +- Fixed bots being able to take items from secure cabinets if the item is inside another container inside the cabinet. +- Fixes to hull generation between docking ports. Should fix tiny gaps between the hulls that caused characters to briefly teleport outside the sub when passing through the port, and hulls generating incorrectly when the main submarine has hulls at both sides of the docking port. +- Fixed captain's pipe, harmonica and pipe tobacco not spawning in crates when purchased. +- Fixed console errors when trying to overwrite an existing item assembly in the sub editor. +- Fixed the margin calculations that in some cases made it impossible for Bonethreshers (and possibly other monsters) to reach a moving target. +- Fixed CargoManager only spawning one crate of each type and dropping the rest of the items on the floor in multiplayer. +- Fixed abyss monsters sometimes being unable to hit the sub and just keep pushing the sub. +- Define "tool", "weapon", and "provocative" targeting params to be ignored if the creature is not in the same sub as the target. Fixes some odd cases where the monsters e.g. target some item inside the sub when they are outside. +- Fixed occasional excessive camera shake when a large monster is lodged between the sub and the level. +- Fixed ballast flora not getting cleared from respawn shuttles when the shuttle despawns. +- Fixed ability to leave the level with a pirate sub. +- Fixed turret hardpoints appearing in the minimap view when giving an Operate Weapons order. +- Fixed ability to switch back to a sub you've left behind using the outpost terminals. +- Fixed doors without integrated buttons getting instantly opened by clicking on them when you have access to the button. +- Fixed chaingun barrel's sprite depth not being affected by the depth of the item, causing the barrels to be drawn in front of the item if it's depth is larger than 0.13. +- Fixed ability to switch control to the hostages with Z and X keys during hostage missions. +- Fixed abyss and combat suits not getting autofilled with oxygen tanks when placing the initial supplies to a sub. +- Fixed previously selected missions reappearing when you retry a level, even though they don't show up in the mission selection menu. +- Fixed inability to type in the "max players" field in the "host server" tab. +- Fixed rotated weapons with a different origin getting misaligned when swapping them. +- Fixed item sell quantity sometimes appearing "maxed out" at a quantity less than what the player is actually selling. +- Fixed crashing when the team id argument for the "spawn" console command is formatted incorrectly, mention the argument in the command's help text. +- Docking port whack-a-mole part n: fixed respawn shuttle's ports not showing up on the sonar. +- More descriptive error message when a client fails to spawn an entity due to it's ID being taken up by something else ("Failed to read event for entity EntitySpawner (ID xxx taken by yyyy)"). +- Fixed cargo mission reward displaying as 0 in the popups at the start of a round. +- Fixed clients being able to toggle missions on/off without campaign management permissions. +- Fixed bots trying to clean up seeds and put items into bags that were moved to somewhere the bot can't access them, causing the bot to be stuck trying to reach the target. +- Fixed wrecks and beacons sometimes spawning too close to the start and end positions when there's no outpost present. +- Fixed "Ignore This" order affecting all NPCs. It should only affect the bots in a player team. +- Fixed "Ignore This" order restricting player access to doors. +- Fixed bots targeting same targets when fixing/repairing. +- Fixed monsters sometimes getting stuck "dancing" near the submarine because they try to avoid and target it at the same time. +- Fixed cold caverns always having a wreck. There's now 50% chance for a wreck in cold caverns. +- Monster events: Fixed many higher difficulty subsets spawning multiple monster events when they should spawn only one +- Monster events: Fixed Watcher spawns post difficulty level 20 trigger the event cooldown when they shouldn't +- Fixed inability to talk to the "Husk Cultist" event NPC when returning to them a second time. (Other events could have been affected, too.) +- Fixed security guards starting to shoot each other if one of them accidentally does damage on any friendly character (Only unstable). + +Modding: +- Fixed overriding nav terminals with a mod resetting the changes to the custom interface buttons. +- Fixed status effect conditionals that check a parent container's tags checking the container's components, not just the item itself. Resulted in tag inequality checks always succeeding, because the components don't have any tags. +- Fixed crashing if a scripted event tries to spawn a human prefab that can't be found (i.e. if a mod uses a non-existent human prefab identifier). +- RemoveItemAction can be used to remove tagged items directly, not just items in a tagged character's inventory. +- Replaced "spawnprobability" attribute in monster events with a generic "probability" attribute that can be used for any type of event. +- Option to spawn particles across the ray cast from a hitscan projectile (see "pulselaserbolt"). +- Fixed linked subs included in outpost modules being positioned incorrectly. +- Added "swapidentifier" to SwappableItem definitions. Can be used to restrict which items in a given category can be swapped with each other (e.g. if you want to add custom upgradeable turrets but not make them swappable with the vanilla turrets). +- Made SecondaryUse status effects work with ranged weapons. +- Added "equal" targeting tag to enemy AI. Makes it possible to define how monsters react to monsters with the same combat strength. +- TagAction can be used to tag characters based on the human prefab identifier. +- Fixed a crash in the character editor that happened when a humanoid character had an elbow joint but didn't (yet) have a wrist joint. +- The "avoid gun fire" parameter now defaults to false. +- Added "equal" targeting tag for monster AI definitions. + +--------------------------------------------------------------------------------------------------------- v0.1400.1.0 (unstable) --------------------------------------------------------------------------------------------------------- @@ -73,7 +150,7 @@ Modding: - Option to configure the number of gates between biomes (see MapGenerationParameters.xml). - Fixed "publishing [mod] in the steam workshop failed" error when trying to publish a mod that contains enemy subs. ------------------------------------------------------------------------------------------------------- +--------------------------------------------------------------------------------------------------------- v0.1400.0.0 (unstable) --------------------------------------------------------------------------------------------------------- @@ -125,7 +202,7 @@ Modding: - Renamed "Inflitrate" to "CanOpenDoors" and rewrote the tooltip. - Added "scalemultiplier" (Vector2) for particle emitters so that the effects can be scaled just in one axis instead of both. ------------------------------------------------------------------------------------------------------- +--------------------------------------------------------------------------------------------------------- v0.13.3.11 ---------------------------------------------------------------------------------------------------------