diff --git a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs index 11e5a5bd4..b38f63525 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs @@ -47,6 +47,8 @@ namespace Barotrauma set { maxZoom = MathHelper.Clamp(value, 1.0f, 10.0f); } } + public float FreeCamMoveSpeed = 1.0f; + private float zoom; private float offsetAmount; @@ -197,10 +199,15 @@ namespace Barotrauma private void CreateMatrices() { - resolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); - worldView = new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight); - viewMatrix = Matrix.CreateTranslation(new Vector3(GameMain.GraphicsWidth / 2.0f, GameMain.GraphicsHeight / 2.0f, 0)); - + SetResolution(new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight)); + } + + public void SetResolution(Point res) + { + resolution = res; + + worldView = new Rectangle(0, 0, res.X, res.Y); + viewMatrix = Matrix.CreateTranslation(new Vector3(res.X / 2.0f, res.Y / 2.0f, 0)); globalZoomScale = (float)Math.Pow(new Vector2(GUI.UIWidth, resolution.Y).Length() / GUI.ReferenceResolution.Length(), 2); } @@ -265,17 +272,17 @@ namespace Barotrauma { if (GUI.KeyboardDispatcher.Subscriber == null) { - if (PlayerInput.KeyDown(Keys.LeftShift)) moveSpeed *= 2.0f; - if (PlayerInput.KeyDown(Keys.LeftControl)) moveSpeed *= 0.5f; + if (PlayerInput.KeyDown(Keys.LeftShift)) { moveSpeed *= 2.0f; } + if (PlayerInput.KeyDown(Keys.LeftControl)) { moveSpeed *= 0.5f; } - if (GameMain.Config.KeyBind(InputType.Left).IsDown()) moveInput.X -= 1.0f; - if (GameMain.Config.KeyBind(InputType.Right).IsDown()) moveInput.X += 1.0f; - if (GameMain.Config.KeyBind(InputType.Down).IsDown()) moveInput.Y -= 1.0f; - if (GameMain.Config.KeyBind(InputType.Up).IsDown()) moveInput.Y += 1.0f; + if (GameMain.Config.KeyBind(InputType.Left).IsDown()) { moveInput.X -= 1.0f; } + if (GameMain.Config.KeyBind(InputType.Right).IsDown()) { moveInput.X += 1.0f; } + if (GameMain.Config.KeyBind(InputType.Down).IsDown()) { moveInput.Y -= 1.0f; } + if (GameMain.Config.KeyBind(InputType.Up).IsDown()) { moveInput.Y += 1.0f; } } velocity = Vector2.Lerp(velocity, moveInput, deltaTime * 10.0f); - moveCam = velocity * moveSpeed * deltaTime * 60.0f; + moveCam = velocity * moveSpeed * deltaTime * FreeCamMoveSpeed * 60.0f; if (Screen.Selected == GameMain.GameScreen && FollowSub) { @@ -291,14 +298,21 @@ namespace Barotrauma { Vector2 mouseInWorld = ScreenToWorld(PlayerInput.MousePosition); Vector2 diffViewCenter; - diffViewCenter = ((mouseInWorld - Position) * Zoom); + diffViewCenter = (mouseInWorld - Position) * Zoom; targetZoom = MathHelper.Clamp( - targetZoom + (PlayerInput.ScrollWheelSpeed / 1000.0f) * zoom, + targetZoom + PlayerInput.ScrollWheelSpeed / 1000.0f * zoom, GameMain.DebugDraw ? MinZoom * 0.1f : MinZoom, MaxZoom); - Zoom = MathHelper.Lerp(Zoom, targetZoom, deltaTime * 10.0f); - if (!PlayerInput.KeyDown(Keys.F)) Position = mouseInWorld - (diffViewCenter / Zoom); + if (PlayerInput.KeyDown(Keys.LeftControl)) + { + Zoom += (targetZoom - zoom) / (ZoomSmoothness * 10.0f); + } + else + { + Zoom = MathHelper.Lerp(Zoom, targetZoom, deltaTime * 10.0f); + } + if (!PlayerInput.KeyDown(Keys.F)) { Position = mouseInWorld - (diffViewCenter / Zoom); } } } else if (allowMove) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs index 075153900..cff4a5b16 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs @@ -31,57 +31,54 @@ namespace Barotrauma GUI.DrawString(spriteBatch, pos + textOffset, Character.Name, Color.White, Color.Black); - if (ObjectiveManager != null) + var currentOrder = ObjectiveManager.CurrentOrder; + if (currentOrder != null) { - var currentOrder = ObjectiveManager.CurrentOrder; - if (currentOrder != null) + GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(0, 20), $"ORDER: {currentOrder.DebugTag} ({currentOrder.Priority.FormatZeroDecimal()})", Color.White, Color.Black); + } + else if (ObjectiveManager.WaitTimer > 0) + { + GUI.DrawString(spriteBatch, pos + new Vector2(0, 20), $"Waiting... {ObjectiveManager.WaitTimer.FormatZeroDecimal()}", Color.White, Color.Black); + } + var currentObjective = ObjectiveManager.CurrentObjective; + if (currentObjective != null) + { + int offset = currentOrder != null ? 20 : 0; + if (currentOrder == null || currentOrder.Priority <= 0) { - GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(0, 20), $"ORDER: {currentOrder.DebugTag} ({currentOrder.Priority.FormatZeroDecimal()})", Color.White, Color.Black); + GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(0, 20 + offset), $"MAIN OBJECTIVE: {currentObjective.DebugTag} ({currentObjective.Priority.FormatZeroDecimal()})", Color.White, Color.Black); } - else if (ObjectiveManager.WaitTimer > 0) + var subObjective = currentObjective.CurrentSubObjective; + if (subObjective != null) { - GUI.DrawString(spriteBatch, pos + new Vector2(0, 20), $"Waiting... {ObjectiveManager.WaitTimer.FormatZeroDecimal()}", Color.White, Color.Black); + GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(0, 40 + offset), $"SUBOBJECTIVE: {subObjective.DebugTag} ({subObjective.Priority.FormatZeroDecimal()})", Color.White, Color.Black); } - var currentObjective = ObjectiveManager.CurrentObjective; - if (currentObjective != null) + var activeObjective = ObjectiveManager.GetActiveObjective(); + if (activeObjective != null) { - int offset = currentOrder != null ? 20 : 0; - if (currentOrder == null || currentOrder.Priority <= 0) - { - GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(0, 20 + offset), $"MAIN OBJECTIVE: {currentObjective.DebugTag} ({currentObjective.Priority.FormatZeroDecimal()})", Color.White, Color.Black); - } - var subObjective = currentObjective.CurrentSubObjective; - if (subObjective != null) - { - GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(0, 40 + offset), $"SUBOBJECTIVE: {subObjective.DebugTag} ({subObjective.Priority.FormatZeroDecimal()})", Color.White, Color.Black); - } - var activeObjective = ObjectiveManager.GetActiveObjective(); - if (activeObjective != null) - { - GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(0, 60 + offset), $"ACTIVE OBJECTIVE: {activeObjective.DebugTag} ({activeObjective.Priority.FormatZeroDecimal()})", Color.White, Color.Black); - } + GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(0, 60 + offset), $"ACTIVE OBJECTIVE: {activeObjective.DebugTag} ({activeObjective.Priority.FormatZeroDecimal()})", Color.White, Color.Black); } - for (int i = 0; i < ObjectiveManager.Objectives.Count; i++) + } + for (int i = 0; i < ObjectiveManager.Objectives.Count; i++) + { + var objective = ObjectiveManager.Objectives[i]; + int offsetMultiplier; + if (ObjectiveManager.CurrentOrder == null) { - var objective = ObjectiveManager.Objectives[i]; - int offsetMultiplier; - if (ObjectiveManager.CurrentOrder == null) + if (i == 0) { - if (i == 0) - { - continue; - } - else - { - offsetMultiplier = i - 1; - } + continue; } else { - offsetMultiplier = i + 1; + offsetMultiplier = i - 1; } - GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(120, offsetMultiplier * 18 + 100), $"{objective.DebugTag} ({objective.Priority.FormatZeroDecimal()})", Color.White, Color.Black * 0.5f); } + else + { + offsetMultiplier = i + 1; + } + GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(120, offsetMultiplier * 18 + 100), $"{objective.DebugTag} ({objective.Priority.FormatZeroDecimal()})", Color.White, Color.Black * 0.5f); } if (steeringManager is IndoorsSteeringManager pathSteering) @@ -116,6 +113,14 @@ namespace Barotrauma GUI.DrawLine(spriteBatch, pos, pos + ConvertUnits.ToDisplayUnits(new Vector2(Character.AnimController.TargetMovement.X, -Character.AnimController.TargetMovement.Y)), Color.SteelBlue, width: 2); GUI.DrawLine(spriteBatch, pos, pos + ConvertUnits.ToDisplayUnits(new Vector2(Steering.X, -Steering.Y)), Color.Blue, width: 3); + if (Character.AnimController.InWater && objectiveManager.GetActiveObjective() is AIObjectiveGoTo gotoObjective && gotoObjective.TargetGap != null) + { + Vector2 gapPosition = gotoObjective.TargetGap.WorldPosition; + gapPosition.Y = -gapPosition.Y; + GUI.DrawRectangle(spriteBatch, gapPosition - new Vector2(10.0f, 10.0f), new Vector2(20.0f, 20.0f), Color.Orange, false); + GUI.DrawLine(spriteBatch, pos, gapPosition, Color.Orange * 0.5f, 0, 5); + } + //if (Character.IsKeyDown(InputType.Aim)) //{ // GUI.DrawLine(spriteBatch, pos, new Vector2(Character.CursorWorldPosition.X, -Character.CursorWorldPosition.Y), Color.Yellow, width: 4); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs index 69be757fd..a51ce4cc1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs @@ -481,13 +481,13 @@ namespace Barotrauma var controller = character.SelectedConstruction?.GetComponent(); if (controller != null && controller.ControlCharacterPose && controller.User == character) { - if (controller.Item.SpriteDepth > maxDepth) + if (controller.Item.SpriteDepth <= maxDepth || controller.DrawUserBehind) { - depthOffset = Math.Max(controller.Item.SpriteDepth - 0.0001f - maxDepth, 0.0f); + depthOffset = Math.Max(controller.Item.GetDrawDepth() + 0.0001f - minDepth, -minDepth); } else { - depthOffset = Math.Max(controller.Item.SpriteDepth + 0.0001f - minDepth, -minDepth); + depthOffset = Math.Max(controller.Item.GetDrawDepth() - 0.0001f - maxDepth, 0.0f); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs index 84d76d1e8..ad368d0aa 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Attack.cs @@ -46,7 +46,7 @@ namespace Barotrauma if (sound != null) { - SoundPlayer.PlaySound(sound.Sound, worldPosition, sound.Volume, sound.Range); + SoundPlayer.PlaySound(sound.Sound, worldPosition, sound.Volume, sound.Range, ignoreMuffling: sound.IgnoreMuffling); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index fe09303c5..07f9c4823 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -457,7 +457,7 @@ namespace Barotrauma if (draggingItemToWorld) { if (item.OwnInventory == null || - !item.OwnInventory.CanBePut(CharacterInventory.draggingItem) || + !item.OwnInventory.CanBePut(CharacterInventory.DraggingItems.First()) || !CanAccessInventory(item.OwnInventory)) { continue; @@ -561,7 +561,7 @@ namespace Barotrauma { if (InvisibleTimer > 0.0f) { - if (Controlled == null || (Controlled.CharacterHealth.GetAffliction("psychosis")?.Strength ?? 0.0f) <= 0.0f) + if (Controlled == null || Controlled == this || (Controlled.CharacterHealth.GetAffliction("psychosis")?.Strength ?? 0.0f) <= 0.0f) { InvisibleTimer = 0.0f; } @@ -579,15 +579,15 @@ namespace Barotrauma { soundTimer -= deltaTime; } - else if (AIController != null) + else if (AIController is EnemyAIController enemyAI) { - switch (AIController.State) + switch (enemyAI.State) { case AIState.Attack: PlaySound(CharacterSound.SoundType.Attack); break; default: - var petBehavior = (AIController as EnemyAIController)?.PetBehavior; + var petBehavior = enemyAI.PetBehavior; if (petBehavior != null && petBehavior.Happiness < petBehavior.MaxHappiness * 0.25f) { PlaySound(CharacterSound.SoundType.Unhappy); @@ -741,7 +741,7 @@ namespace Barotrauma if (speechBubbleTimer > 0.0f) { - GUI.SpeechBubbleIcon.Draw(spriteBatch, pos - Vector2.UnitY * 30, + GUI.SpeechBubbleIcon.Draw(spriteBatch, pos - Vector2.UnitY * 5, speechBubbleColor * Math.Min(speechBubbleTimer, 1.0f), 0.0f, Math.Min(speechBubbleTimer, 1.0f)); } @@ -803,7 +803,7 @@ namespace Barotrauma Color nameColor = Color.White; if (Controlled != null && TeamID != Controlled.TeamID) { - nameColor = TeamID == TeamType.FriendlyNPC ? Color.SkyBlue : GUI.Style.Red; + nameColor = TeamID == CharacterTeamType.FriendlyNPC ? Color.SkyBlue : GUI.Style.Red; } if (CampaignInteractionType != CampaignMode.InteractionType.None && AllowCustomInteract) { @@ -902,7 +902,7 @@ namespace Barotrauma } var selectedSound = matchingSounds.GetRandom(); if (selectedSound?.Sound == null) { return; } - soundChannel = SoundPlayer.PlaySound(selectedSound.Sound, AnimController.WorldPosition, selectedSound.Volume, selectedSound.Range, hullGuess: CurrentHull); + soundChannel = SoundPlayer.PlaySound(selectedSound.Sound, AnimController.WorldPosition, selectedSound.Volume, selectedSound.Range, hullGuess: CurrentHull, ignoreMuffling: selectedSound.IgnoreMuffling); soundTimer = soundInterval; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index e640ad831..3b2b2fe3f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -57,7 +57,7 @@ namespace Barotrauma !ConversationAction.FadeScreenToBlack; } - private static string GetCachedHudText(string textTag, string keyBind) + public static string GetCachedHudText(string textTag, string keyBind) { if (cachedHudTexts.TryGetValue(textTag + keyBind, out string text)) { @@ -76,10 +76,10 @@ namespace Barotrauma { if (character.Inventory != null) { - for (int i = 0; i < character.Inventory.Items.Length - 1; i++) + for (int i = 0; i < character.Inventory.Capacity; i++) { - var item = character.Inventory.Items[i]; - if (item == null || character.Inventory.SlotTypes[i] == InvSlotType.Any) continue; + var item = character.Inventory.GetItemAt(i); + if (item == null || character.Inventory.SlotTypes[i] == InvSlotType.Any) { continue; } foreach (ItemComponent ic in item.Components) { @@ -131,10 +131,10 @@ namespace Barotrauma character.Inventory.ClearSubInventories(); } - for (int i = 0; i < character.Inventory.Items.Length - 1; i++) + for (int i = 0; i < character.Inventory.Capacity; i++) { - var item = character.Inventory.Items[i]; - if (item == null || character.Inventory.SlotTypes[i] == InvSlotType.Any) continue; + var item = character.Inventory.GetItemAt(i); + if (item == null || character.Inventory.SlotTypes[i] == InvSlotType.Any) { continue; } foreach (ItemComponent ic in item.Components) { @@ -206,22 +206,31 @@ namespace Barotrauma orderIndicatorCount.Clear(); foreach (Pair activeOrder in GameMain.GameSession.CrewManager.ActiveOrders) { + if (!DrawIcon(activeOrder.First)) { continue; } + if (activeOrder.Second.HasValue) { DrawOrderIndicator(spriteBatch, cam, character, activeOrder.First, iconAlpha: MathHelper.Clamp(activeOrder.Second.Value / 10.0f, 0.2f, 1.0f)); } else { - float iconAlpha = GetDistanceBasedIconAlpha(activeOrder.First.TargetSpatialEntity, maxDistance: 350.0f); + float iconAlpha = GetDistanceBasedIconAlpha(activeOrder.First.TargetSpatialEntity, maxDistance: 450.0f); if (iconAlpha <= 0.0f) { continue; } - DrawOrderIndicator(spriteBatch, cam, character, activeOrder.First, iconAlpha: iconAlpha, createOffset: false, scaleMultiplier: 0.5f); + DrawOrderIndicator(spriteBatch, cam, character, activeOrder.First, + iconAlpha: iconAlpha, createOffset: false, scaleMultiplier: 0.5f, overrideAlpha: true); } } - if (character.CurrentOrder != null) + if (DrawIcon(character.CurrentOrder)) { DrawOrderIndicator(spriteBatch, cam, character, character.CurrentOrder, 1.0f); } + + static bool DrawIcon(Order o) => + o != null && + (!(o.TargetEntity is Item i) || + o.DrawIconWhenContained || + i.GetRootInventoryOwner() == i); } foreach (Character.ObjectiveEntity objectiveEntity in character.ActiveObjectiveEntities) @@ -231,7 +240,7 @@ namespace Barotrauma foreach (Item brokenItem in brokenItems) { - if (brokenItem.NonInteractable) { continue; } + if (!brokenItem.IsInteractable(character)) { continue; } float alpha = GetDistanceBasedIconAlpha(brokenItem); if (alpha <= 0.0f) continue; GUI.DrawIndicator(spriteBatch, brokenItem.DrawPosition, cam, 100.0f, GUI.BrokenIcon, @@ -244,7 +253,7 @@ namespace Barotrauma return Math.Min((maxDistance - dist) / maxDistance * 2.0f, 1.0f); } - if (!character.IsIncapacitated && character.Stun <= 0.0f && !IsCampaignInterfaceOpen && (!character.IsKeyDown(InputType.Aim) || character.SelectedItems.Any(it => it?.GetComponent() == null))) + if (!character.IsIncapacitated && character.Stun <= 0.0f && !IsCampaignInterfaceOpen && (!character.IsKeyDown(InputType.Aim) || character.HeldItems.Any(it => it?.GetComponent() == null))) { if (character.FocusedCharacter != null && character.FocusedCharacter.CanBeSelected) { @@ -281,7 +290,7 @@ namespace Barotrauma if (!GUI.DisableItemHighlights && !Inventory.DraggingItemToWorld) { bool shiftDown = PlayerInput.KeyDown(Keys.LeftShift) || PlayerInput.KeyDown(Keys.RightShift); - if(shouldRecreateHudTexts || heldDownShiftWhenGotHudTexts != shiftDown) + if (shouldRecreateHudTexts || heldDownShiftWhenGotHudTexts != shiftDown) { shouldRecreateHudTexts = true; heldDownShiftWhenGotHudTexts = shiftDown; @@ -291,8 +300,8 @@ namespace Barotrauma int dir = Math.Sign(focusedItem.WorldPosition.X - character.WorldPosition.X); - Vector2 textSize = GUI.Font.MeasureString(focusedItem.Name); - Vector2 largeTextSize = GUI.SubHeadingFont.MeasureString(focusedItem.Name); + Vector2 textSize = GUI.Font.MeasureString(hudTexts.First().Text); + Vector2 largeTextSize = GUI.SubHeadingFont.MeasureString(hudTexts.First().Text); Vector2 startPos = cam.WorldToScreen(focusedItem.DrawPosition); startPos.Y -= (hudTexts.Count + 1) * textSize.Y; @@ -307,11 +316,11 @@ namespace Barotrauma float alpha = MathHelper.Clamp((focusedItemOverlayTimer - ItemOverlayDelay) * 2.0f, 0.0f, 1.0f); - GUI.DrawString(spriteBatch, textPos, focusedItem.Name, GUI.Style.TextColor * alpha, Color.Black * alpha * 0.7f, 2, font: GUI.SubHeadingFont); + GUI.DrawString(spriteBatch, textPos, hudTexts.First().Text, hudTexts.First().Color * alpha, Color.Black * alpha * 0.7f, 2, font: GUI.SubHeadingFont); startPos.X += dir * 10.0f * GUI.Scale; textPos.X += dir * 10.0f * GUI.Scale; textPos.Y += largeTextSize.Y; - foreach (ColoredText coloredText in hudTexts) + foreach (ColoredText coloredText in hudTexts.Skip(1)) { if (dir == -1) textPos.X = (int)(startPos.X - GUI.SmallFont.MeasureString(coloredText.Text).X); GUI.DrawString(spriteBatch, textPos, coloredText.Text, coloredText.Color * alpha, Color.Black * alpha * 0.7f, 2, GUI.SmallFont); @@ -341,9 +350,8 @@ namespace Barotrauma } if (Character.Controlled.Inventory != null) { - foreach (Item item in Character.Controlled.Inventory.Items) + foreach (Item item in Character.Controlled.Inventory.AllItems) { - if (item == null) { continue; } if (Character.Controlled.HasEquippedItem(item)) { item.DrawHUD(spriteBatch, cam, Character.Controlled); @@ -355,10 +363,10 @@ namespace Barotrauma if (character.Inventory != null) { - for (int i = 0; i < character.Inventory.Items.Length - 1; i++) + for (int i = 0; i < character.Inventory.Capacity; i++) { - var item = character.Inventory.Items[i]; - if (item == null || character.Inventory.SlotTypes[i] == InvSlotType.Any) continue; + var item = character.Inventory.GetItemAt(i); + if (item == null || character.Inventory.SlotTypes[i] == InvSlotType.Any) { continue; } foreach (ItemComponent ic in item.Components) { @@ -432,12 +440,16 @@ namespace Barotrauma private static void DrawCharacterHoverTexts(SpriteBatch spriteBatch, Camera cam, Character character) { - foreach (Item item in character.Inventory.Items) + var allItems = character.Inventory?.AllItems; + if (allItems != null) { - var statusHUD = item?.GetComponent(); - if (statusHUD != null && statusHUD.IsActive && statusHUD.VisibleCharacters.Contains(character.FocusedCharacter)) + foreach (Item item in allItems) { - return; + var statusHUD = item?.GetComponent(); + if (statusHUD != null && statusHUD.IsActive && statusHUD.VisibleCharacters.Contains(character.FocusedCharacter)) + { + return; + } } } @@ -454,7 +466,7 @@ namespace Barotrauma Color nameColor = GUI.Style.TextColor; if (character.TeamID != character.FocusedCharacter.TeamID) { - nameColor = character.FocusedCharacter.TeamID == Character.TeamType.FriendlyNPC ? Color.SkyBlue : GUI.Style.Red; + nameColor = character.FocusedCharacter.TeamID == CharacterTeamType.FriendlyNPC ? Color.SkyBlue : GUI.Style.Red; } GUI.DrawString(spriteBatch, textPos, focusName, nameColor, Color.Black * 0.7f, 2, GUI.SubHeadingFont); @@ -493,7 +505,9 @@ namespace Barotrauma return character.ShouldLockHud(); } - private static void DrawOrderIndicator(SpriteBatch spriteBatch, Camera cam, Character character, Order order, float iconAlpha = 1.0f, bool createOffset = true, float scaleMultiplier = 1.0f) + /// Override the distance-based alpha value with the iconAlpha parameter value + private static void DrawOrderIndicator(SpriteBatch spriteBatch, Camera cam, Character character, Order order, + float iconAlpha = 1.0f, bool createOffset = true, float scaleMultiplier = 1.0f, bool overrideAlpha = false) { if (order?.SymbolSprite == null) { return; } if (order.IsReport && order.OrderGiver != character && !order.HasAppropriateJob(character)) { return; } @@ -514,7 +528,8 @@ namespace Barotrauma Vector2 drawPos = target is Entity ? (target as Entity).DrawPosition : target.Submarine == null ? target.Position : target.Position + target.Submarine.DrawPosition; drawPos += Vector2.UnitX * order.SymbolSprite.size.X * 1.5f * orderIndicatorCount[target]; - GUI.DrawIndicator(spriteBatch, drawPos, cam, 100.0f, order.SymbolSprite, order.Color * iconAlpha, createOffset: createOffset, scaleMultiplier: scaleMultiplier); + GUI.DrawIndicator(spriteBatch, drawPos, cam, 100.0f, order.SymbolSprite, order.Color * iconAlpha, + createOffset: createOffset, scaleMultiplier: scaleMultiplier, overrideAlpha: overrideAlpha ? (float?)iconAlpha : null); orderIndicatorCount[target] = orderIndicatorCount[target] + 1; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index 332b80c91..2427989ad 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -152,34 +152,19 @@ namespace Barotrauma partial void OnSkillChanged(string skillIdentifier, float prevLevel, float newLevel, Vector2 textPopupPos) { - if (TeamID == Character.TeamType.FriendlyNPC) { return; } + if (TeamID == CharacterTeamType.FriendlyNPC) { return; } if (Character.Controlled != null && Character.Controlled.TeamID != TeamID) { return; } - if (newLevel - prevLevel > 0.1f) - { - GUI.AddMessage( - "+" + ((int)((newLevel - prevLevel) * 100.0f)).ToString() + " XP", - GUI.Style.Green, - textPopupPos, - Vector2.UnitY * 10.0f, - playSound: false); - } - else if (prevLevel % 0.1f > 0.05f && newLevel % 0.1f < 0.05f) - { - GUI.AddMessage( - "+10 XP", - GUI.Style.Green, - textPopupPos, - Vector2.UnitY * 10.0f, - playSound: false); - } - if ((int)newLevel > (int)prevLevel) { + int increase = Math.Max((int)newLevel - (int)prevLevel, 1); GUI.AddMessage( - TextManager.GetWithVariables("SkillIncreased", new string[3] { "[name]", "[skillname]", "[newlevel]" }, - new string[3] { Name, TextManager.Get("SkillName." + skillIdentifier), ((int)newLevel).ToString() }, - new bool[3] { false, true, false }), GUI.Style.Green); + string.Format("+{0} {1}", increase, TextManager.Get("SkillName." + skillIdentifier)), + GUI.Style.Green, + textPopupPos, + Vector2.UnitY * 10.0f, + playSound: false, + subId: Character?.Submarine?.ID ?? -1); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index e02363bf8..630ec6bd2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -349,7 +349,7 @@ namespace Barotrauma { string skillIdentifier = msg.ReadString(); float skillLevel = msg.ReadSingle(); - info?.SetSkillLevel(skillIdentifier, skillLevel, WorldPosition + Vector2.UnitY * 150.0f); + info?.SetSkillLevel(skillIdentifier, skillLevel, Position + Vector2.UnitY * 150.0f); } break; case 4: //NetEntityEvent.Type.ExecuteAttack @@ -434,7 +434,7 @@ namespace Barotrauma CharacterInfo info = CharacterInfo.ClientRead(infoSpeciesName, inc); character = Create(speciesName, position, seed, characterInfo: info, id: id, isRemotePlayer: ownerId > 0 && GameMain.Client.ID != ownerId, hasAi: hasAi); - character.TeamID = (TeamType)teamID; + character.TeamID = (CharacterTeamType)teamID; character.CampaignInteractionType = (CampaignMode.InteractionType)inc.ReadByte(); if (character.CampaignInteractionType != CampaignMode.InteractionType.None) { @@ -487,7 +487,7 @@ namespace Barotrauma character.ReadStatus(inc); } - if (character.IsHuman && character.TeamID != TeamType.FriendlyNPC && !character.IsDead) + if (character.IsHuman && character.TeamID != CharacterTeamType.FriendlyNPC && !character.IsDead) { CharacterInfo duplicateCharacterInfo = GameMain.GameSession.CrewManager.GetCharacterInfos().FirstOrDefault(c => c.ID == info.ID); GameMain.GameSession.CrewManager.RemoveCharacterInfo(duplicateCharacterInfo); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterSound.cs index c247da8e5..3cea60b80 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterSound.cs @@ -18,6 +18,11 @@ namespace Barotrauma public float Range => roundSound == null ? 0.0f : roundSound.Range; public Sound Sound => roundSound?.Sound; + public bool IgnoreMuffling + { + get { return roundSound?.IgnoreMuffling ?? false; } + } + public CharacterSound(CharacterParams.SoundParams soundParams) { Params = soundParams; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/HUDProgressBar.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/HUDProgressBar.cs index 0ebc33f9b..5052e74fe 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/HUDProgressBar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/HUDProgressBar.cs @@ -72,7 +72,7 @@ namespace Barotrauma FadeTimer = 1.0f; if (!string.IsNullOrEmpty(textTag)) { - textTag = textTag; + this.textTag = textTag; Text = TextManager.Get(textTag); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs index cbef93fdc..5835cd675 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs @@ -10,6 +10,7 @@ namespace Barotrauma { partial void UpdateMessages() { + if (Prefab is AfflictionPrefabHusk { SendMessages: false }) { return; } switch (State) { case InfectionState.Dormant: diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index 6b855f999..d059cf7f1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -912,7 +912,7 @@ namespace Barotrauma lowSkillIndicator.Color = new Color(lowSkillIndicator.Color, MathHelper.Lerp(0.5f, 1.0f, (float)(Math.Sin(Timing.TotalTime * 5.0f) + 1.0f) / 2.0f)); - if (Inventory.draggingItem != null) + if (Inventory.DraggingItems.Any()) { if (highlightedLimbIndex > -1) { @@ -1632,8 +1632,8 @@ namespace Barotrauma } //can't apply treatment to dead characters - if (Character.IsDead) return true; - if (item == null || !item.UseInHealthInterface) return true; + if (Character.IsDead) { return true; } + if (item == null || !item.UseInHealthInterface) { return true; } if (!ignoreMousePos) { if (highlightedLimbIndex > -1) @@ -1652,33 +1652,25 @@ namespace Barotrauma private List GetAvailableMedicalItems() { List allInventoryItems = new List(); - allInventoryItems.AddRange(Character.Inventory.Items); + allInventoryItems.AddRange(Character.Inventory.AllItems); if (Character.SelectedCharacter?.Inventory != null && Character.CanAccessInventory(Character.SelectedCharacter.Inventory)) { - allInventoryItems.AddRange(Character.SelectedCharacter.Inventory.Items); + allInventoryItems.AddRange(Character.SelectedCharacter.Inventory.AllItems); } if (Character.SelectedBy?.Inventory != null) { - allInventoryItems.AddRange(Character.SelectedBy.Inventory.Items); + allInventoryItems.AddRange(Character.SelectedBy.Inventory.AllItems); } - List medicalItems = new List(); foreach (Item item in allInventoryItems) { - if (item == null) continue; - - var containedItems = item.ContainedItems; - if (containedItems != null) + foreach (Item containedItem in item.ContainedItems) { - foreach (Item containedItem in containedItems) - { - if (containedItem == null) continue; - if (!containedItem.HasTag("medical") && !containedItem.HasTag("chem")) continue; - medicalItems.Add(containedItem); - } + if (!containedItem.HasTag("medical") && !containedItem.HasTag("chem")) { continue; } + medicalItems.Add(containedItem); } - if (!item.HasTag("medical") && !item.HasTag("chem")) continue; + if (!item.HasTag("medical") && !item.HasTag("chem")) { continue; } medicalItems.Add(item); } @@ -1804,24 +1796,27 @@ namespace Barotrauma spriteBatch.Begin(SpriteSortMode.Deferred, Lights.CustomBlendStates.Multiplicative); - float overlayScale = Math.Min( - drawArea.Width / (float)limbIndicatorOverlay.FrameSize.X, - drawArea.Height / (float)limbIndicatorOverlay.FrameSize.Y); - - int frame = 0; - int frameCount = 17; - if (limbIndicatorOverlayAnimState >= frameCount * 2) limbIndicatorOverlayAnimState = 0.0f; - if (limbIndicatorOverlayAnimState < frameCount) + if (limbIndicatorOverlay != null) { - frame = (int)limbIndicatorOverlayAnimState; - } - else - { - frame = frameCount - (int)(limbIndicatorOverlayAnimState - (frameCount - 1)); - } + float overlayScale = Math.Min( + drawArea.Width / (float)limbIndicatorOverlay.FrameSize.X, + drawArea.Height / (float)limbIndicatorOverlay.FrameSize.Y); - limbIndicatorOverlay.Draw(spriteBatch, frame, drawArea.Center.ToVector2(), Color.Gray, origin: limbIndicatorOverlay.FrameSize.ToVector2() / 2, rotate: 0.0f, - scale: Vector2.One * overlayScale); + int frame = 0; + int frameCount = 17; + if (limbIndicatorOverlayAnimState >= frameCount * 2) limbIndicatorOverlayAnimState = 0.0f; + if (limbIndicatorOverlayAnimState < frameCount) + { + frame = (int)limbIndicatorOverlayAnimState; + } + else + { + frame = frameCount - (int)(limbIndicatorOverlayAnimState - (frameCount - 1)); + } + + limbIndicatorOverlay.Draw(spriteBatch, frame, drawArea.Center.ToVector2(), Color.Gray, origin: limbIndicatorOverlay.FrameSize.ToVector2() / 2, rotate: 0.0f, + scale: Vector2.One * overlayScale); + } if (allowHighlight) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs index 7581cd84d..01caadf07 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs @@ -22,8 +22,8 @@ namespace Barotrauma float strength = MathHelper.Lerp(0, 1, MathUtils.InverseLerp(0, MathHelper.Pi, diff)); float jointAngle = JointAngle * strength; - JointBendDeformation limbADeformation = LimbA.Deformations.Find(d => d is JointBendDeformation) as JointBendDeformation; - JointBendDeformation limbBDeformation = LimbB.Deformations.Find(d => d is JointBendDeformation) as JointBendDeformation; + JointBendDeformation limbADeformation = LimbA.ActiveDeformations.Find(d => d is JointBendDeformation) as JointBendDeformation; + JointBendDeformation limbBDeformation = LimbB.ActiveDeformations.Find(d => d is JointBendDeformation) as JointBendDeformation; if (limbADeformation != null && limbBDeformation != null) { @@ -114,7 +114,10 @@ namespace Barotrauma /// Note that different limbs can share the same deformations. /// Use ragdoll.SpriteDeformations for a collection that cannot have duplicates. /// - public List Deformations { get; private set; } = new List(); + private List Deformations { get; set; } = new List(); + private List NonConditionalDeformations { get; set; } = new List(); + private List<(ConditionalSprite, IEnumerable)> ConditionalDeformations { get; set; } = new List<(ConditionalSprite, IEnumerable)>(); + public List ActiveDeformations { get; set; } = new List(); public Sprite Sprite { get; protected set; } @@ -178,6 +181,9 @@ namespace Barotrauma { public float RotationState; public float OffsetState; + public Vector2 RandomOffsetMultiplier = new Vector2(Rand.Range(-1.0f, 1.0f), Rand.Range(-1.0f, 1.0f)); + public float RandomRotationFactor = Rand.Range(0.0f, 1.0f); + public float RandomScaleFactor = Rand.Range(0.0f, 1.0f); public bool IsActive = true; } @@ -282,12 +288,16 @@ namespace Barotrauma ConditionalSprites.Add(conditionalSprite); if (conditionalSprite.DeformableSprite != null) { - CreateDeformations(subElement.GetChildElement("deformablesprite")); + var conditionalDeformations = CreateDeformations(subElement.GetChildElement("deformablesprite")); + Deformations.AddRange(conditionalDeformations); + ConditionalDeformations.Add((conditionalSprite, conditionalDeformations)); } break; case "deformablesprite": _deformSprite = new DeformableSprite(subElement, filePath: GetSpritePath(subElement, Params.deformSpriteParams)); - CreateDeformations(subElement); + var deformations = CreateDeformations(subElement); + Deformations.AddRange(deformations); + NonConditionalDeformations.AddRange(deformations); break; case "lightsource": LightSource = new LightSource(subElement, GetConditionalTarget()) @@ -315,8 +325,9 @@ namespace Barotrauma return targetEntity; } - void CreateDeformations(XElement e) + IEnumerable CreateDeformations(XElement e) { + List deformations = new List(); foreach (XElement animationElement in e.GetChildElements("spritedeformation")) { int sync = animationElement.GetAttributeInt("sync", -1); @@ -340,14 +351,39 @@ namespace Barotrauma } if (deformation != null) { - Deformations.Add(deformation); + deformations.Add(deformation); } } + return deformations; } } LightSource?.CheckConditionals(); } + private void RefreshDeformations() + { + if (_deformSprite == null) { return; } + if (ConditionalSprites.None()) + { + ActiveDeformations = Deformations; + } + else + { + ActiveDeformations.Clear(); + if (_deformSprite == DeformSprite) + { + ActiveDeformations.AddRange(NonConditionalDeformations); + } + foreach (var conditionalDeformation in ConditionalDeformations) + { + if (conditionalDeformation.Item1.IsActive) + { + ActiveDeformations.AddRange(conditionalDeformation.Item2); + } + } + } + } + public void RecreateSprites() { if (Sprite != null) @@ -390,18 +426,24 @@ namespace Barotrauma character.Info?.CalculateHeadPosition(sprite); } + private string _texturePath; private string GetSpritePath(XElement element, SpriteParams spriteParams) { - if (spriteParams != null) + if (_texturePath == null) { - return GetSpritePath(spriteParams.GetTexturePath()); - } - else - { - string texturePath = element.GetAttributeString("texture", null); - texturePath = string.IsNullOrWhiteSpace(texturePath) ? ragdoll.RagdollParams.Texture : texturePath; - return GetSpritePath(texturePath); + if (spriteParams != null) + { + string texturePath = character.Params.VariantFile?.Root?.GetAttributeString("texture", null) ?? spriteParams.GetTexturePath(); + _texturePath = GetSpritePath(texturePath); + } + else + { + string texturePath = element.GetAttributeString("texture", null); + texturePath = string.IsNullOrWhiteSpace(texturePath) ? ragdoll.RagdollParams.Texture : texturePath; + _texturePath = GetSpritePath(texturePath); + } } + return _texturePath; } /// @@ -537,7 +579,7 @@ namespace Barotrauma else { var spriteParams = Params.GetSprite(); - if (spriteParams.DeadColorTime > 0 && deadTimer < spriteParams.DeadColorTime) + if (spriteParams != null && spriteParams.DeadColorTime > 0 && deadTimer < spriteParams.DeadColorTime) { deadTimer += deltaTime; } @@ -587,6 +629,7 @@ namespace Barotrauma } UpdateSpriteStates(deltaTime); + RefreshDeformations(); } public void Draw(SpriteBatch spriteBatch, Camera cam, Color? overrideColor = null) @@ -637,13 +680,13 @@ namespace Barotrauma var deformSprite = DeformSprite; if (deformSprite != null) { - if (Deformations != null && Deformations.Any()) + if (ActiveDeformations.Any()) { - var deformation = SpriteDeformation.GetDeformation(Deformations, deformSprite.Size); + var deformation = SpriteDeformation.GetDeformation(ActiveDeformations, deformSprite.Size); deformSprite.Deform(deformation); if (LightSource != null && LightSource.DeformableLightSprite != null) { - deformation = SpriteDeformation.GetDeformation(Deformations, deformSprite.Size, dir == Direction.Left); + deformation = SpriteDeformation.GetDeformation(ActiveDeformations, deformSprite.Size, dir == Direction.Left); LightSource.DeformableLightSprite.Deform(deformation); } } @@ -666,9 +709,9 @@ namespace Barotrauma if (conditionalSprite.DeformableSprite != null) { var defSprite = conditionalSprite.DeformableSprite; - if (Deformations != null && Deformations.Any()) + if (ActiveDeformations.Any()) { - var deformation = SpriteDeformation.GetDeformation(Deformations, defSprite.Size); + var deformation = SpriteDeformation.GetDeformation(ActiveDeformations, defSprite.Size); defSprite.Deform(deformation); } else @@ -705,13 +748,13 @@ namespace Barotrauma c = Color.Lerp(c, spriteParams.DeadColor, MathUtils.InverseLerp(0, Params.GetSprite().DeadColorTime, deadTimer)); } c = overrideColor ?? c; - float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState); - Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState) * Scale; + float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState, spriteAnimState[decorativeSprite].RandomRotationFactor); + Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier) * Scale; var ca = (float)Math.Cos(-body.Rotation); var sa = (float)Math.Sin(-body.Rotation); Vector2 transformedOffset = new Vector2(ca * offset.X + sa * offset.Y, -sa * offset.X + ca * offset.Y); decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(body.DrawPosition.X + transformedOffset.X, -(body.DrawPosition.Y + transformedOffset.Y)), c, - -body.Rotation + rotation, decorativeSprite.Scale * Scale, spriteEffect, + -body.Rotation + rotation, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, spriteEffect, depth: decorativeSprite.Sprite.Depth); } float depthStep = 0.000001f; diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index 5992eab99..31345e1ba 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -13,6 +13,8 @@ using System.Globalization; using FarseerPhysics; using Barotrauma.Extensions; using Barotrauma.Steam; +using System.Threading.Tasks; +using Barotrauma.MapCreatures.Behavior; namespace Barotrauma { @@ -69,6 +71,8 @@ namespace Barotrauma private static readonly ChatManager chatManager = new ChatManager(true, 64); + public static Dictionary Keybinds = new Dictionary(); + public static void Init() { OpenAL.Alc.SetErrorReasonCallback((string msg) => NewMessage(msg, Color.Orange)); @@ -145,6 +149,17 @@ namespace Barotrauma } } + if (!IsOpen && GUI.KeyboardDispatcher.Subscriber == null) + { + foreach (var (key, command) in Keybinds) + { + if (PlayerInput.KeyHit(key)) + { + ExecuteCommand(command); + } + } + } + activeQuestionText?.SetAsLastChild(); if (PlayerInput.KeyHit(Keys.F3)) @@ -227,6 +242,13 @@ namespace Barotrauma case "fpscounter": case "dumptofile": case "findentityids": + case "setfreecamspeed": + case "togglevoicechatfilters": + case "bindkey": + case "savebinds": + case "unbindkey": + case "wikiimage_character": + case "wikiimage_sub": return true; default: return client.HasConsoleCommandPermission(command); @@ -235,20 +257,23 @@ namespace Barotrauma public static void DequeueMessages() { - while (queuedMessages.Count > 0) + lock (queuedMessages) { - var newMsg = queuedMessages.Dequeue(); - if (listBox == null) + while (queuedMessages.Count > 0) { - //don't attempt to add to the listbox if it hasn't been created yet - Messages.Add(newMsg); - } - else - { - AddMessage(newMsg); - } + var newMsg = queuedMessages.Dequeue(); + if (listBox == null) + { + //don't attempt to add to the listbox if it hasn't been created yet + Messages.Add(newMsg); + } + else + { + AddMessage(newMsg); + } - if (GameSettings.SaveDebugConsoleLogs) unsavedMessages.Add(newMsg); + if (GameSettings.SaveDebugConsoleLogs) unsavedMessages.Add(newMsg); + } } } @@ -487,6 +512,24 @@ namespace Barotrauma GameMain.CharacterEditorScreen.Select(); })); + commands.Add(new Command("quickstart", "Starts a singleplayer sandbox", (string[] args) => + { + if (Screen.Selected != GameMain.MainMenuScreen) + { + ThrowError("This command can only be executed from the main menu."); + return; + } + + string subName = args.Length > 0 ? args[0] : ""; + if (string.IsNullOrWhiteSpace(subName)) + { + ThrowError("No submarine specified."); + return; + } + + GameMain.MainMenuScreen.QuickStart(fixedSeed: false, subName); + }, getValidArgs: () => new[] { SubmarineInfo.SavedSubmarines.Select(s => s.Name).Distinct().ToArray() })); + commands.Add(new Command("steamnetdebug", "steamnetdebug: Toggles Steamworks networking debug logging.", (string[] args) => { SteamManager.NetworkingDebugLog = !SteamManager.NetworkingDebugLog; @@ -497,6 +540,97 @@ namespace Barotrauma { NewMessage("Ready checks can only be commenced in multiplayer.", Color.Red); })); + + commands.Add(new Command("bindkey", "bindkey [key] [command]: Binds a key to a command.", (string[] args) => + { + if (args.Length < 2) + { + ThrowError("No key or command specified."); + return; + } + + string keyString = args[0]; + string command = args[1]; + + if (Enum.TryParse(typeof(Keys), keyString, ignoreCase: true, out object outKey) && outKey is Keys key) + { + if (Keybinds.ContainsKey(key)) + { + Keybinds[key] = command; + } + else + { + Keybinds.Add(key, command); + } + NewMessage($"\"{command}\" bound to {key}.", GUI.Style.Green); + + if (GameMain.Config.keyMapping.FirstOrDefault(bind => bind.Key != Keys.None && bind.Key == key) is { } existingBind) + { + AddWarning($"\"{key}\" has already been bound to {(InputType)GameMain.Config.keyMapping.IndexOf(existingBind)}. The keybind will perform both actions when pressed."); + } + + return; + } + + ThrowError($"Invalid key {keyString}."); + }, isCheat: false, getValidArgs: () => new[] { Enum.GetNames(typeof(Keys)), new[] { "\"\"" } })); + + commands.Add(new Command("unbindkey", "unbindkey [key]: Unbinds a command.", (string[] args) => + { + if (args.Length < 1) + { + ThrowError("No key specified."); + return; + } + + string keyString = args[0]; + if (Enum.TryParse(typeof(Keys), keyString, ignoreCase: true, out object outKey) && outKey is Keys key) + { + if (Keybinds.ContainsKey(key)) + { + Keybinds.Remove(key); + } + NewMessage("Keybind unbound.", GUI.Style.Green); + return; + } + ThrowError($"Invalid key {keyString}."); + }, isCheat: false, getValidArgs: () => new[] { Keybinds.Keys.Select(keys => keys.ToString()).Distinct().ToArray() })); + + commands.Add(new Command("savebinds", "savebinds: Writes current keybinds into the config file.", (string[] args) => + { + ShowQuestionPrompt($"Some keybinds may render the game unusable, are you sure you want to make these keybinds persistent? ({Keybinds.Count} keybind(s) assigned) Y/N", + (option2) => + { + if (option2.ToLower() != "y") + { + NewMessage("Aborted.", GUI.Style.Red); + return; + } + + GameSettings.ConsoleKeybinds = new Dictionary(Keybinds); + GameMain.Config.SaveNewPlayerConfig(); + + NewMessage($"{Keybinds.Count} keybind(s) written to the config file.", GUI.Style.Green); + }); + }, isCheat: false)); + + commands.Add(new Command("togglegrid", "Toggle visual snap grid in sub editor.", (string[] args) => + { + SubEditorScreen.ShouldDrawGrid = !SubEditorScreen.ShouldDrawGrid; + NewMessage(SubEditorScreen.ShouldDrawGrid ? "Enabled submarine grid." : "Disabled submarine grid.", GUI.Style.Green); + })); + + commands.Add(new Command("wikiimage_character", "Save an image of the currently controlled character with a transparent background.", (string[] args) => + { + if (Character.Controlled == null) { return; } + WikiImage.Create(Character.Controlled); + })); + + commands.Add(new Command("wikiimage_sub", "Save an image of the main submarine with a transparent background.", (string[] args) => + { + if (Submarine.MainSub == null) { return; } + WikiImage.Create(Submarine.MainSub); + })); AssignRelayToServer("kick", false); AssignRelayToServer("kickid", false); @@ -510,11 +644,18 @@ namespace Barotrauma AssignRelayToServer("verboselogging", false); AssignRelayToServer("freecam", false); AssignRelayToServer("steamnetdebug", false); + AssignRelayToServer("quickstart", false); + AssignRelayToServer("togglegrid", false); + AssignRelayToServer("bindkey", false); + AssignRelayToServer("unbindkey", false); + AssignRelayToServer("savebinds", false); #if DEBUG AssignRelayToServer("crash", false); + AssignRelayToServer("showballastflorasprite", false); AssignRelayToServer("simulatedlatency", false); AssignRelayToServer("simulatedloss", false); AssignRelayToServer("simulatedduplicateschance", false); + AssignRelayToServer("storeinfo", false); #endif commands.Add(new Command("clientlist", "", (string[] args) => { })); @@ -998,6 +1139,17 @@ namespace Barotrauma }); AssignRelayToServer("debugdraw", false); + AssignOnExecute("togglevoicechatfilters", (string[] args) => + { + if (args.None() || !bool.TryParse(args[0], out bool state)) + { + state = !GameMain.Config.DisableVoiceChatFilters; + } + GameMain.Config.DisableVoiceChatFilters = state; + NewMessage("Voice chat filters " + (GameMain.Config.DisableVoiceChatFilters ? "disabled" : "enabled"), Color.White); + }); + AssignRelayToServer("togglevoicechatfilters", false); + commands.Add(new Command("fpscounter", "fpscounter: Toggle the FPS counter.", (string[] args) => { GameMain.ShowFPS = !GameMain.ShowFPS; @@ -1383,6 +1535,16 @@ namespace Barotrauma File.WriteAllLines(filePath, debugLines); ToolBox.OpenFileWithShell(Path.GetFullPath(filePath)); })); + + commands.Add(new Command("setfreecamspeed", "setfreecamspeed [speed]: Set the camera movement speed when not controlling a character. Defaults to 1.", (string[] args) => + { + if (args.Length > 0) + { + float.TryParse(args[0], NumberStyles.Number, CultureInfo.InvariantCulture, out float speed); + Screen.Selected.Cam.FreeCamMoveSpeed = speed; + } + })); + #if DEBUG commands.Add(new Command("setplanthealth", "setplanthealth [value]: Sets the health of the selected plant in sub editor.", (string[] args) => { @@ -1417,6 +1579,12 @@ namespace Barotrauma } })); + commands.Add(new Command("showballastflorasprite", "", (string[] args) => + { + BallastFloraBehavior.AlwaysShowBallastFloraSprite = !BallastFloraBehavior.AlwaysShowBallastFloraSprite; + NewMessage("ok", GUI.Style.Green); + })); + commands.Add(new Command("printreceivertransfers", "", (string[] args) => { GameMain.Client.PrintReceiverTransters(); @@ -1834,13 +2002,15 @@ namespace Barotrauma #if DEBUG commands.Add(new Command("querylobbies", "Queries all SteamP2P lobbies", (args) => { - Steamworks.Data.LobbyQuery lobbyQuery = Steamworks.SteamMatchmaking.CreateLobbyQuery().FilterDistanceWorldwide(); - - Steamworks.Data.Lobby[] lobbies = lobbyQuery.RequestAsync().Result; - foreach (var lobby in lobbies) - { - DebugConsole.NewMessage(lobby.GetData("name") + ", " + lobby.GetData("lobbyowner")); - } + TaskPool.Add("DebugQueryLobbies", + SteamManager.LobbyQueryRequest(), (t) => { + var lobbies = ((Task>)t).Result; + foreach (var lobby in lobbies) + { + NewMessage(lobby.GetData("name") + ", " + lobby.GetData("lobbyowner"), Color.Yellow); + } + NewMessage($"Retrieved a total of {lobbies.Count} lobbies", Color.Lime); + }); })); commands.Add(new Command("checkduplicates", "Checks the given language for duplicate translation keys and writes to file.", (string[] args) => diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs index a3270bafa..fe31bb320 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventActions/ConversationAction.cs @@ -210,9 +210,33 @@ namespace Barotrauma } }; + double allowCloseTime = Timing.TotalTime + 0.5; closeButton.Children.ForEach(child => child.SpriteEffects = SpriteEffects.FlipVertically); closeButton.Frame.FadeIn(0.5f, 0.5f); closeButton.SlideIn(0.5f, 0.33f, 16, SlideDirection.Down); + + InputType? closeInput = null; + if (GameMain.Config.KeyBind(InputType.Use).MouseButton == MouseButton.None) + { + closeInput = InputType.Use; + } + else if (GameMain.Config.KeyBind(InputType.Select).MouseButton == MouseButton.None) + { + closeInput = InputType.Select; + } + if (closeInput.HasValue) + { + closeButton.ToolTip = TextManager.ParseInputTypes($"{TextManager.Get("Close")} ([InputType.{closeInput.Value}])"); + closeButton.OnAddedToGUIUpdateList += (GUIComponent component) => + { + if (Timing.TotalTime > allowCloseTime && PlayerInput.KeyHit(closeInput.Value)) + { + GUIButton btn = component as GUIButton; + btn?.OnClicked(btn, btn.UserData); + btn?.Flash(GUI.Style.Green); + } + }; + } } for (int i = 0; i < optionButtons.Count; i++) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs index cbd48fd79..31c5b8913 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs @@ -43,15 +43,15 @@ namespace Barotrauma } GUI.DrawString(spriteBatch, new Vector2(10, y), "EventManager", Color.White, Color.Black * 0.6f, 0, GUI.SmallFont); - GUI.DrawString(spriteBatch, new Vector2(15, y + 20), "Event cooldown: " + eventCoolDown, Color.White, Color.Black * 0.6f, 0, GUI.SmallFont); - GUI.DrawString(spriteBatch, new Vector2(15, y + 35), "Current intensity: " + (int) Math.Round(currentIntensity * 100), Color.Lerp(Color.White, GUI.Style.Red, currentIntensity), Color.Black * 0.6f, 0, GUI.SmallFont); - GUI.DrawString(spriteBatch, new Vector2(15, y + 50), "Target intensity: " + (int) Math.Round(targetIntensity * 100), Color.Lerp(Color.White, GUI.Style.Red, targetIntensity), Color.Black * 0.6f, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(15, y + 20), "Event cooldown: " + (int)Math.Max(eventCoolDown, 0), Color.White, Color.Black * 0.6f, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(15, y + 35), "Current intensity: " + (int)Math.Round(currentIntensity * 100), Color.Lerp(Color.White, GUI.Style.Red, currentIntensity), Color.Black * 0.6f, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(15, y + 50), "Target intensity: " + (int)Math.Round(targetIntensity * 100), Color.Lerp(Color.White, GUI.Style.Red, targetIntensity), Color.Black * 0.6f, 0, GUI.SmallFont); - GUI.DrawString(spriteBatch, new Vector2(15, y + 65), "AvgHealth: " + (int) Math.Round(avgCrewHealth * 100), Color.Lerp(GUI.Style.Red, GUI.Style.Green, avgCrewHealth), Color.Black * 0.6f, 0, GUI.SmallFont); - GUI.DrawString(spriteBatch, new Vector2(15, y + 80), "AvgHullIntegrity: " + (int) Math.Round(avgHullIntegrity * 100), Color.Lerp(GUI.Style.Red, GUI.Style.Green, avgHullIntegrity), Color.Black * 0.6f, 0, GUI.SmallFont); - GUI.DrawString(spriteBatch, new Vector2(15, y + 95), "FloodingAmount: " + (int) Math.Round(floodingAmount * 100), Color.Lerp(GUI.Style.Green, GUI.Style.Red, floodingAmount), Color.Black * 0.6f, 0, GUI.SmallFont); - GUI.DrawString(spriteBatch, new Vector2(15, y + 110), "FireAmount: " + (int) Math.Round(fireAmount * 100), Color.Lerp(GUI.Style.Green, GUI.Style.Red, fireAmount), Color.Black * 0.6f, 0, GUI.SmallFont); - GUI.DrawString(spriteBatch, new Vector2(15, y + 125), "EnemyDanger: " + (int) Math.Round(enemyDanger * 100), Color.Lerp(GUI.Style.Green, GUI.Style.Red, enemyDanger), Color.Black * 0.6f, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(15, y + 65), "AvgHealth: " + (int)Math.Round(avgCrewHealth * 100), Color.Lerp(GUI.Style.Red, GUI.Style.Green, avgCrewHealth), Color.Black * 0.6f, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(15, y + 80), "AvgHullIntegrity: " + (int)Math.Round(avgHullIntegrity * 100), Color.Lerp(GUI.Style.Red, GUI.Style.Green, avgHullIntegrity), Color.Black * 0.6f, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(15, y + 95), "FloodingAmount: " + (int)Math.Round(floodingAmount * 100), Color.Lerp(GUI.Style.Green, GUI.Style.Red, floodingAmount), Color.Black * 0.6f, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(15, y + 110), "FireAmount: " + (int)Math.Round(fireAmount * 100), Color.Lerp(GUI.Style.Green, GUI.Style.Red, fireAmount), Color.Black * 0.6f, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(15, y + 125), "EnemyDanger: " + (int)Math.Round(enemyDanger * 100), Color.Lerp(GUI.Style.Green, GUI.Style.Red, enemyDanger), Color.Black * 0.6f, 0, GUI.SmallFont); #if DEBUG if (PlayerInput.KeyDown(Microsoft.Xna.Framework.Input.Keys.LeftAlt) && @@ -86,15 +86,26 @@ namespace Barotrauma new Vector2(graphRect.Right + 5, graphRect.Y + graphRect.Height * (1.0f - eventThreshold)), Color.Orange, 0, 1); y = graphRect.Bottom + 20; - if (eventCoolDown > 0.0f) + int x = graphRect.X; + if (isCrewAway && crewAwayDuration < settings.FreezeDurationWhenCrewAway) { - GUI.DrawString(spriteBatch, new Vector2(graphRect.X, y), "Event cooldown active: " + (int) eventCoolDown, Color.LightGreen * 0.8f, null, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(x, y), "Events frozen (crew away from sub): " + ToolBox.SecondsToReadableTime(settings.FreezeDurationWhenCrewAway - crewAwayDuration), Color.LightGreen * 0.8f, null, 0, GUI.SmallFont); + y += 15; + } + else if (crewAwayResetTimer > 0.0f) + { + GUI.DrawString(spriteBatch, new Vector2(x, y), "Events frozen (crew just returned to the sub): " + ToolBox.SecondsToReadableTime(crewAwayResetTimer), Color.LightGreen * 0.8f, null, 0, GUI.SmallFont); + y += 15; + } + else if (eventCoolDown > 0.0f) + { + GUI.DrawString(spriteBatch, new Vector2(x, y), "Event cooldown active: " + ToolBox.SecondsToReadableTime(eventCoolDown), Color.LightGreen * 0.8f, null, 0, GUI.SmallFont); y += 15; } else if (currentIntensity > eventThreshold) { - GUI.DrawString(spriteBatch, new Vector2(graphRect.X, y), - "Intensity too high for new events: " + (int) (currentIntensity * 100) + "%/" + (int) (eventThreshold * 100) + "%", Color.LightGreen * 0.8f, null, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(x, y), + "Intensity too high for new events: " + (int)(currentIntensity * 100) + "%/" + (int)(eventThreshold * 100) + "%", Color.LightGreen * 0.8f, null, 0, GUI.SmallFont); y += 15; } @@ -102,22 +113,27 @@ namespace Barotrauma { if (Submarine.MainSub == null) { break; } - GUI.DrawString(spriteBatch, new Vector2(graphRect.X, y), "New event (ID " + eventSet.DebugIdentifier + ") after: ", Color.Orange * 0.8f, null, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(x, y), "New event (ID " + eventSet.DebugIdentifier + ") after: ", Color.Orange * 0.8f, null, 0, GUI.SmallFont); y += 12; + if (eventSet.PerCave) + { + GUI.DrawString(spriteBatch, new Vector2(x, y), " submarine near cave", Color.Orange * 0.8f, null, 0, GUI.SmallFont); + y += 12; + } if (eventSet.PerWreck) { - GUI.DrawString(spriteBatch, new Vector2(graphRect.X, y), " submarine near the wreck", Color.Orange * 0.8f, null, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(x, y), " submarine near the wreck", Color.Orange * 0.8f, null, 0, GUI.SmallFont); y += 12; } if (eventSet.PerRuin) { - GUI.DrawString(spriteBatch, new Vector2(graphRect.X, y), " submarine near the ruins", Color.Orange * 0.8f, null, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(x, y), " submarine near the ruins", Color.Orange * 0.8f, null, 0, GUI.SmallFont); y += 12; } if (roundDuration < eventSet.MinMissionTime) { - GUI.DrawString(spriteBatch, new Vector2(graphRect.X, y), + GUI.DrawString(spriteBatch, new Vector2(x, y), " " + (int) (eventSet.MinDistanceTraveled * 100.0f) + "% travelled (current: " + (int) (distanceTraveled * 100.0f) + " %)", ((Submarine.MainSub == null || distanceTraveled < eventSet.MinDistanceTraveled) ? Color.Lerp(GUI.Style.Yellow, GUI.Style.Red, eventSet.MinDistanceTraveled - distanceTraveled) : GUI.Style.Green) * 0.8f, null, 0, GUI.SmallFont); y += 12; @@ -125,7 +141,7 @@ namespace Barotrauma if (CurrentIntensity < eventSet.MinIntensity || CurrentIntensity > eventSet.MaxIntensity) { - GUI.DrawString(spriteBatch, new Vector2(graphRect.X, y), + GUI.DrawString(spriteBatch, new Vector2(x, y), " intensity between " + ((int) eventSet.MinIntensity) + " and " + ((int) eventSet.MaxIntensity), Color.Orange * 0.8f, null, 0, GUI.SmallFont); y += 12; @@ -133,22 +149,28 @@ namespace Barotrauma if (roundDuration < eventSet.MinMissionTime) { - GUI.DrawString(spriteBatch, new Vector2(graphRect.X, y), + GUI.DrawString(spriteBatch, new Vector2(x, y), " " + (int) (eventSet.MinMissionTime - roundDuration) + " s", Color.Lerp(GUI.Style.Yellow, GUI.Style.Red, (eventSet.MinMissionTime - roundDuration)), null, 0, GUI.SmallFont); } y += 15; + + if (y > GameMain.GraphicsHeight * 0.9f) + { + y = graphRect.Bottom + 35; + x += 250; + } } - GUI.DrawString(spriteBatch, new Vector2(graphRect.X, y), "Current events: ", Color.White * 0.9f, null, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(x, y), "Current events: ", Color.White * 0.9f, null, 0, GUI.SmallFont); y += 15; foreach (Event ev in activeEvents.Where(ev => !ev.IsFinished || PlayerInput.IsShiftDown())) { - GUI.DrawString(spriteBatch, new Vector2(graphRect.X + 5, y), ev.ToString(), (!ev.IsFinished ? Color.White : Color.Red) * 0.8f, null, 0, GUI.SmallFont); + GUI.DrawString(spriteBatch, new Vector2(x + 5, y), ev.ToString(), (!ev.IsFinished ? Color.White : Color.Red) * 0.8f, null, 0, GUI.SmallFont); - Rectangle rect = new Rectangle(new Point(graphRect.X + 5, y), GUI.SmallFont.MeasureString(ev.ToString()).ToPoint()); + Rectangle rect = new Rectangle(new Point(x + 5, y), GUI.SmallFont.MeasureString(ev.ToString()).ToPoint()); Rectangle outlineRect = new Rectangle(rect.Location, rect.Size); outlineRect.Inflate(4, 4); @@ -176,6 +198,11 @@ namespace Barotrauma } y += 18; + if (y > GameMain.GraphicsHeight * 0.9f) + { + y = graphRect.Bottom + 35; + x += 250; + } } } @@ -333,7 +360,7 @@ namespace Barotrauma $"Spawn pending: {artifactEvent.SpawnPending.ColorizeObject()}\n" + $"Spawn position: {artifactEvent.SpawnPos.ColorizeObject()}\n"; - if (artifactEvent.Item != null) + if (artifactEvent.Item != null && !artifactEvent.Item.Removed) { Vector2 pos = artifactEvent.Item.WorldPosition; positions.Add(new DebugLine(pos, Color.White)); @@ -364,7 +391,8 @@ namespace Barotrauma foreach (Character monster in monsterEvent.Monsters) { - text += $" {monster.ColorizeObject()} -> (Dead: {monster.IsDead.ColorizeObject()}, Health: {monster.HealthPercentage.ColorizeObject()}%, AIState: {(monster.AIController?.State).ColorizeObject()})\n"; + text += $" {monster.ColorizeObject()} -> (Dead: {monster.IsDead.ColorizeObject()}, Health: {monster.HealthPercentage.ColorizeObject()}%, AIState: {(monster.AIController is EnemyAIController enemyAI ? enemyAI.State : AIState.Idle ).ColorizeObject()})\n"; + if (monster.Removed) { continue; } positions.Add(new DebugLine(monster.WorldPosition, Color.Red)); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/BeaconMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/BeaconMission.cs index 167e254de..584e13d3e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/BeaconMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/BeaconMission.cs @@ -1,7 +1,4 @@ using Barotrauma.Networking; -using System; -using System.Collections.Generic; -using System.Text; namespace Barotrauma { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CombatMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CombatMission.cs index 2c1ab95a6..fc8cafcd4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CombatMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CombatMission.cs @@ -17,7 +17,7 @@ namespace Barotrauma } //team specific - return descriptions[GameMain.Client.Character.TeamID == Character.TeamType.Team1 ? 1 : 2]; + return descriptions[GameMain.Client.Character.TeamID == CharacterTeamType.Team1 ? 1 : 2]; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs index b8e18a7c3..61e23e9a5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs @@ -12,6 +12,23 @@ namespace Barotrauma { public override void ClientReadInitial(IReadMessage msg) { + byte caveCount = msg.ReadByte(); + for (int i = 0; i < caveCount; i++) + { + byte selectedCave = msg.ReadByte(); + if (selectedCave < 255 && Level.Loaded != null) + { + if (selectedCave < Level.Loaded.Caves.Count) + { + Level.Loaded.Caves[selectedCave].DisplayOnSonar = true; + } + else + { + DebugConsole.ThrowError($"Cave index out of bounds when reading nest mission data. Index: {selectedCave}, number of caves: {Level.Loaded.Caves.Count}"); + } + } + } + for (int i = 0; i < ResourceClusters.Count; i++) { var amount = msg.ReadByte(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/NestMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/NestMission.cs index b8f29883f..9de9c32c0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/NestMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/NestMission.cs @@ -8,9 +8,23 @@ namespace Barotrauma { public override void ClientReadInitial(IReadMessage msg) { + byte selectedCaveIndex = msg.ReadByte(); nestPosition = new Vector2( msg.ReadSingle(), msg.ReadSingle()); + if (selectedCaveIndex < 255 && Level.Loaded != null) + { + if (selectedCaveIndex < Level.Loaded.Caves.Count) + { + Level.Loaded.Caves[selectedCaveIndex].DisplayOnSonar = true; + SpawnNestObjects(Level.Loaded, Level.Loaded.Caves[selectedCaveIndex]); + } + else + { + DebugConsole.ThrowError($"Cave index out of bounds when reading nest mission data. Index: {selectedCaveIndex}, number of caves: {Level.Loaded.Caves.Count}"); + } + } + ushort itemCount = msg.ReadUInt16(); for (int i = 0; i < itemCount; i++) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index 3c92b6bf4..e9d244dbf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -287,6 +287,7 @@ namespace Barotrauma { lock (mutex) { + usedIndicatorAngles.Clear(); if (ScreenChanged) { @@ -1117,15 +1118,26 @@ namespace Barotrauma /// Set the cursor to an hourglass. /// Will automatically revert after 10 seconds or when is called. /// - public static void SetCursorWaiting() + public static void SetCursorWaiting(int waitSeconds = 10, Func endCondition = null) { CoroutineManager.StartCoroutine(WaitCursorCoroutine(), "WaitCursorTimeout"); - static IEnumerable WaitCursorCoroutine() + IEnumerable WaitCursorCoroutine() { MouseCursor = CursorState.Waiting; - var timeOut = DateTime.Now + new TimeSpan(0, 0, 10); - while (DateTime.Now < timeOut) { yield return CoroutineStatus.Running; } + var timeOut = DateTime.Now + new TimeSpan(0, 0, waitSeconds); + while (DateTime.Now < timeOut) + { + if (endCondition != null) + { + try + { + if (endCondition.Invoke()) { break; } + } + catch { break; } + } + yield return CoroutineStatus.Running; + } if (MouseCursor == CursorState.Waiting) { MouseCursor = CursorState.Default; } yield return CoroutineStatus.Success; } @@ -1219,7 +1231,7 @@ namespace Barotrauma { foreach (GUIMessage msg in messages) { - if (msg.WorldSpace) continue; + if (msg.WorldSpace) { continue; } msg.Timer -= deltaTime; if (msg.Size.X > HUDLayoutSettings.MessageAreaTop.Width) @@ -1244,7 +1256,7 @@ namespace Barotrauma foreach (GUIMessage msg in messages) { - if (!msg.WorldSpace) continue; + if (!msg.WorldSpace) { continue; } msg.Timer -= deltaTime; msg.Pos += msg.Velocity * deltaTime; } @@ -1256,18 +1268,21 @@ namespace Barotrauma #region Element drawing + private static List usedIndicatorAngles = new List(); /// Should the indicator move based on the camera position? - public static void DrawIndicator(SpriteBatch spriteBatch, Vector2 worldPosition, Camera cam, float hideDist, Sprite sprite, Color color, bool createOffset = true, float scaleMultiplier = 1.0f) + /// Override the distance-based alpha value with the specified alpha value + public static void DrawIndicator(SpriteBatch spriteBatch, Vector2 worldPosition, Camera cam, float hideDist, Sprite sprite, Color color, + bool createOffset = true, float scaleMultiplier = 1.0f, float? overrideAlpha = null) { Vector2 diff = worldPosition - cam.WorldViewCenter; float dist = diff.Length(); float symbolScale = Math.Min(64.0f / sprite.size.X, 1.0f) * scaleMultiplier; - if (dist > hideDist) + if (overrideAlpha.HasValue || dist > hideDist) { - float alpha = Math.Min((dist - hideDist) / 100.0f, 1.0f); + float alpha = overrideAlpha ?? Math.Min((dist - hideDist) / 100.0f, 1.0f); Vector2 targetScreenPos = cam.WorldToScreen(worldPosition); if (!createOffset) @@ -1279,6 +1294,28 @@ namespace Barotrauma float screenDist = Vector2.Distance(cam.WorldToScreen(cam.WorldViewCenter), targetScreenPos); float angle = MathUtils.VectorToAngle(diff); + float minAngleDiff = 0.05f; + bool overlapFound = true; + int iterations = 0; + while (overlapFound && iterations < 10) + { + overlapFound = false; + foreach (float usedIndicatorAngle in usedIndicatorAngles) + { + float shortestAngle = MathUtils.GetShortestAngle(angle, usedIndicatorAngle); + if (MathUtils.NearlyEqual(shortestAngle, 0.0f)) { shortestAngle = 0.01f; } + if (Math.Abs(shortestAngle) < minAngleDiff) + { + angle -= Math.Sign(shortestAngle) * (minAngleDiff - Math.Abs(shortestAngle)); + overlapFound = true; + break; + } + } + iterations++; + } + + usedIndicatorAngles.Add(angle); + Vector2 unclampedDiff = new Vector2( (float)Math.Cos(angle) * screenDist, (float)-Math.Sin(angle) * screenDist); @@ -1489,12 +1526,12 @@ namespace Barotrauma foreach (GUIMessage msg in messages) { - if (msg.WorldSpace) continue; + if (msg.WorldSpace) { continue; } Vector2 drawPos = new Vector2(HUDLayoutSettings.MessageAreaTop.Right, HUDLayoutSettings.MessageAreaTop.Center.Y); - msg.Font.DrawString(spriteBatch, msg.Text, drawPos + msg.Pos + Vector2.One, Color.Black, 0, msg.Origin, 1.0f, SpriteEffects.None, 0); - msg.Font.DrawString(spriteBatch, msg.Text, drawPos + msg.Pos, msg.Color, 0, msg.Origin, 1.0f, SpriteEffects.None, 0); + msg.Font.DrawString(spriteBatch, msg.Text, drawPos + msg.DrawPos + Vector2.One, Color.Black, 0, msg.Origin, 1.0f, SpriteEffects.None, 0); + msg.Font.DrawString(spriteBatch, msg.Text, drawPos + msg.DrawPos, msg.Color, 0, msg.Origin, 1.0f, SpriteEffects.None, 0); break; } @@ -1507,14 +1544,14 @@ namespace Barotrauma foreach (GUIMessage msg in messages) { - if (!msg.WorldSpace) continue; + if (!msg.WorldSpace) { continue; } if (cam != null) { float alpha = 1.0f; - if (msg.Timer < 1.0f) alpha -= 1.0f - msg.Timer; + if (msg.Timer < 1.0f) { alpha -= 1.0f - msg.Timer; } - Vector2 drawPos = cam.WorldToScreen(msg.Pos); + Vector2 drawPos = cam.WorldToScreen(msg.DrawPos); msg.Font.DrawString(spriteBatch, msg.Text, drawPos + Vector2.One, Color.Black * alpha, 0, msg.Origin, 1.0f, SpriteEffects.None, 0); msg.Font.DrawString(spriteBatch, msg.Text, drawPos, msg.Color * alpha, 0, msg.Origin, 1.0f, SpriteEffects.None, 0); } @@ -1608,7 +1645,8 @@ namespace Barotrauma public static Texture2D CreateCapsule(int radius, int height) { - int textureWidth = radius * 2, textureHeight = height + radius * 2; + int textureWidth = Math.Max(radius * 2, 1); + int textureHeight = Math.Max(height + radius * 2, 1); Color[] data = new Color[textureWidth * textureHeight]; @@ -2079,7 +2117,7 @@ namespace Barotrauma if (pauseMenuOpen) { - Inventory.draggingItem = null; + Inventory.DraggingItems.Clear(); Inventory.DraggingInventory = null; PauseMenu = new GUIFrame(new RectTransform(Vector2.One, Canvas, Anchor.Center), style: null); @@ -2288,9 +2326,10 @@ namespace Barotrauma if (playSound) SoundPlayer.PlayUISound(GUISoundType.UIMessage); } - public static void AddMessage(string message, Color color, Vector2 worldPos, Vector2 velocity, float lifeTime = 3.0f, bool playSound = true, GUISoundType soundType = GUISoundType.UIMessage) + public static void AddMessage(string message, Color color, Vector2 pos, Vector2 velocity, float lifeTime = 3.0f, bool playSound = true, GUISoundType soundType = GUISoundType.UIMessage, int subId = -1) { - messages.Add(new GUIMessage(message, color, worldPos, velocity, lifeTime, Alignment.Center, LargeFont)); + Submarine sub = Submarine.Loaded.FirstOrDefault(s => s.ID == subId); + messages.Add(new GUIMessage(message, color, pos, velocity, lifeTime, Alignment.Center, LargeFont, sub: sub)); if (playSound) SoundPlayer.PlayUISound(soundType); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIColorPicker.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIColorPicker.cs new file mode 100644 index 000000000..5fd93f7da --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIColorPicker.cs @@ -0,0 +1,210 @@ +#nullable enable +using System; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + public class GUIColorPicker : GUIComponent + { + public delegate bool OnColorSelectedHandler(GUIColorPicker component, Color color); + public OnColorSelectedHandler? OnColorSelected; + + public float SelectedHue; + public float SelectedSaturation; + public float SelectedValue; + + public Color CurrentColor = Color.Black; + + private Rectangle MainArea, + HueArea; + + private Texture2D? mainTexture, + hueTexture; + + private Color[]? colorData; + + private Rectangle selectedRect; + + private bool mouseHeld; + private bool isInitialized; + + private readonly Color transparentWhite = Color.White * 0.8f, + transparentBlack = Color.Black * 0.8f; + + public GUIColorPicker(RectTransform rectT, string? style = null) : base(style, rectT) { } + + ~GUIColorPicker() + { + DisposeTextures(); + } + + private void Init() + { + int tWidth = Rect.Width; + int sliceWidth = Rect.Width / 8; + + int mainWidth = tWidth - sliceWidth; + int hueWidth = sliceWidth; + + MainArea = new Rectangle(0, 0, mainWidth, Rect.Height); + HueArea = new Rectangle(mainWidth, 0, hueWidth, Rect.Height); + + colorData = new Color[MainArea.Width * MainArea.Height]; + + if (mainTexture == null) + { + int width = MainArea.Width, + height = MainArea.Height; + + GenerateGradient(ref colorData!, width, height, DrawHVArea); + mainTexture = CreateGradientTexture(colorData!, MainArea.Width, MainArea.Height); + } + + if (hueTexture == null) + { + int width = HueArea.Width, + height = HueArea.Height; + + Color[] hueData = new Color[width * height]; + + GenerateGradient(ref hueData, width, height, DrawHueArea); + hueTexture = CreateGradientTexture(hueData, width, height); + } + } + + protected override void Draw(SpriteBatch spriteBatch) + { + if (mainTexture == null || hueTexture == null || !isInitialized) { return; } + + Rectangle mainArea = MainArea, + hueArea = HueArea; + + hueArea.Location += Rect.Location; + mainArea.Location += Rect.Location; + + Vector2 mainLocation = mainArea.Location.ToVector2(), + hueLocation = hueArea.Location.ToVector2(); + + spriteBatch.Draw(mainTexture, mainLocation, Color.White); + spriteBatch.Draw(hueTexture, hueLocation, Color.White); + + float hueY = hueLocation.Y + ((SelectedHue / 360f) * hueArea.Height); + spriteBatch.DrawLine(hueArea.Left, hueY, hueArea.Right, hueY, transparentWhite, thickness: 3); + spriteBatch.DrawLine(hueArea.Left, hueY, hueArea.Right, hueY, transparentBlack, thickness: 1); + + float saturationX = mainLocation.X + SelectedSaturation * MainArea.Width; + float valueY = mainLocation.Y + (1.0f - SelectedValue) * MainArea.Height; + + spriteBatch.DrawLine(saturationX, mainArea.Top,saturationX, mainArea.Bottom, transparentWhite, thickness: 3); + spriteBatch.DrawLine(mainArea.Left,valueY, mainArea.Right, valueY, transparentWhite, thickness: 3); + + spriteBatch.DrawLine(saturationX, mainArea.Top,saturationX, mainArea.Bottom, transparentBlack, thickness: 1); + spriteBatch.DrawLine(mainArea.Left,valueY, mainArea.Right, valueY, transparentBlack, thickness: 1); + } + + protected override void Update(float deltaTime) + { + base.Update(deltaTime); + + if (!isInitialized) + { + Init(); + isInitialized = true; + } + + if (!PlayerInput.PrimaryMouseButtonHeld()) + { + mouseHeld = false; + } + + if (GUI.MouseOn != this) { return; } + + Rectangle mainArea = MainArea, + hueArea = HueArea; + + hueArea.Location += Rect.Location; + mainArea.Location += Rect.Location; + + if (PlayerInput.PrimaryMouseButtonDown()) + { + mouseHeld = true; + if (hueArea.Contains(PlayerInput.MousePosition)) + { + selectedRect = HueArea; + } + else if (mainArea.Contains(PlayerInput.MousePosition)) + { + selectedRect = MainArea; + } + else + { + mouseHeld = false; + } + } + + if (!PlayerInput.PrimaryMouseButtonHeld()) + { + mouseHeld = false; + } + + if (mouseHeld && (PlayerInput.MouseSpeed != Vector2.Zero || PlayerInput.PrimaryMouseButtonDown())) + { + if (selectedRect == HueArea) + { + Vector2 pos = PlayerInput.MousePosition - hueArea.Location.ToVector2(); + SelectedHue = Math.Clamp(pos.Y / hueArea.Height * 360f, 0, 360); + RefreshHue(); + + } + else if (selectedRect == MainArea) + { + var (x, y) = PlayerInput.MousePosition - mainArea.Location.ToVector2(); + SelectedSaturation = Math.Clamp(x / mainArea.Width, 0, 1); + SelectedValue = Math.Clamp(1f - (y / mainArea.Height), 0, 1); + } + + CurrentColor = ToolBox.HSVToRGB(SelectedHue, SelectedSaturation, SelectedValue); + + OnColorSelected?.Invoke(this, CurrentColor); + } + } + + public void DisposeTextures() + { + mainTexture?.Dispose(); + hueTexture?.Dispose(); + } + + public void RefreshHue() + { + if (colorData == null || mainTexture == null) { return; } + GenerateGradient(ref colorData, mainTexture.Width, mainTexture.Height, DrawHVArea); + mainTexture.SetData(colorData); + } + + private Texture2D CreateGradientTexture(Color[] data, int width, int height) + { + Texture2D texture = new Texture2D(GameMain.GraphicsDeviceManager.GraphicsDevice, width, height); + texture.SetData(data); + return texture; + } + + private void GenerateGradient(ref Color[] data, int width, int height, Func algorithm) + { + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + float relativeX = x / (float) width, + relativeY = y / (float) height; + + data[y * width + x] = algorithm(relativeX, relativeY); + } + } + } + + private Color DrawHVArea(float x, float y) => ToolBox.HSVToRGB(SelectedHue, x, 1.0f - y); + private Color DrawHueArea(float x, float y) => ToolBox.HSVToRGB(y * 360f, 1f, 1f); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs index 5f1c22356..87917f583 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs @@ -510,10 +510,10 @@ namespace Barotrauma for (int i = 0; i < Content.CountChildren; i++) { var child = Content.RectTransform.GetChild(i)?.GUIComponent; - if (child == null) continue; + if (child == null || !child.Visible) { continue; } // selecting - if (Enabled && CanBeFocused && child.CanBeFocused && (GUI.IsMouseOn(child)) && child.Rect.Contains(PlayerInput.MousePosition)) + if (Enabled && CanBeFocused && child.CanBeFocused && child.Rect.Contains(PlayerInput.MousePosition) && GUI.IsMouseOn(child)) { child.State = ComponentState.Hover; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessage.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessage.cs index 97ea742db..2d549b630 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessage.cs @@ -55,6 +55,20 @@ namespace Barotrauma private set; } + public Submarine Submarine + { + get; + private set; + } + + public Vector2 DrawPos + { + get + { + return Submarine == null ? Pos : Pos + Submarine.DrawPosition; + } + } + public GUIMessage(string text, Color color, float lifeTime, ScalableFont font = null) { coloredText = new ColoredText(text, color, false, false); @@ -67,11 +81,11 @@ namespace Barotrauma Font = font; } - public GUIMessage(string text, Color color, Vector2 worldPosition, Vector2 velocity, float lifeTime, Alignment textAlignment = Alignment.Center, ScalableFont font = null) + public GUIMessage(string text, Color color, Vector2 position, Vector2 velocity, float lifeTime, Alignment textAlignment = Alignment.Center, ScalableFont font = null, Submarine sub = null) { coloredText = new ColoredText(text, color, false, false); WorldSpace = true; - pos = worldPosition; + pos = position; Timer = lifeTime; Velocity = velocity; this.lifeTime = lifeTime; @@ -92,6 +106,8 @@ namespace Barotrauma if (textAlignment.HasFlag(Alignment.Bottom)) Origin.Y += size.Y * 0.5f; + + Submarine = sub; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs index 9b4584110..3ff6f35ed 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs @@ -67,6 +67,8 @@ namespace Barotrauma private readonly Type type; + public Type MessageBoxType => type; + public static GUIComponent VisibleBox => MessageBoxes.LastOrDefault(); public GUIMessageBox(string headerText, string text, Vector2? relativeSize = null, Point? minSize = null) @@ -210,6 +212,29 @@ namespace Barotrauma } }; + InputType? closeInput = null; + if (GameMain.Config.KeyBind(InputType.Use).MouseButton == MouseButton.None) + { + closeInput = InputType.Use; + } + else if (GameMain.Config.KeyBind(InputType.Select).MouseButton == MouseButton.None) + { + closeInput = InputType.Select; + } + if (closeInput.HasValue) + { + Buttons[0].ToolTip = TextManager.ParseInputTypes($"{TextManager.Get("Close")} ([InputType.{closeInput.Value}])"); + Buttons[0].OnAddedToGUIUpdateList += (GUIComponent component) => + { + if (!closing && openState >= 1.0f && PlayerInput.KeyHit(closeInput.Value)) + { + GUIButton btn = component as GUIButton; + btn?.OnClicked(btn, btn.UserData); + btn?.Flash(GUI.Style.Green); + } + }; + } + Header = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), Content.RectTransform), headerText, wrap: true); GUI.Style.Apply(Header, "", this); Header.RectTransform.MinSize = new Point(0, Header.Rect.Height); @@ -291,7 +316,8 @@ namespace Barotrauma { if (Draggable) { - if ((GUI.MouseOn == InnerFrame || InnerFrame.IsParentOf(GUI.MouseOn)) && !(GUI.MouseOn is GUIButton)) + GUIComponent parent = GUI.MouseOn?.Parent?.Parent; + if ((GUI.MouseOn == InnerFrame || InnerFrame.IsParentOf(GUI.MouseOn)) && !(GUI.MouseOn is GUIButton || GUI.MouseOn is GUIColorPicker || GUI.MouseOn is GUITextBox || parent is GUITextBox)) { GUI.MouseCursor = CursorState.Move; if (PlayerInput.PrimaryMouseButtonDown()) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs index c05e96d87..58fa65ebb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs @@ -70,6 +70,7 @@ namespace Barotrauma public Color ColorInventoryHalf { get; private set; } = Color.Orange; public Color ColorInventoryFull { get; private set; } = Color.LightGreen; public Color ColorInventoryBackground { get; private set; } = Color.Gray; + public Color ColorInventoryEmptyOverlay { get; private set; } = Color.Red; public Color TextColor { get; private set; } = Color.White * 0.8f; public Color TextColorBright { get; private set; } = Color.White * 0.9f; @@ -150,6 +151,9 @@ namespace Barotrauma case "colorinventorybackground": ColorInventoryBackground = subElement.GetAttributeColor("color", ColorInventoryBackground); break; + case "colorinventoryemptyoverlay": + ColorInventoryEmptyOverlay = subElement.GetAttributeColor("color", ColorInventoryEmptyOverlay); + break; case "textcolordark": TextColorDark = subElement.GetAttributeColor("color", TextColorDark); break; @@ -344,7 +348,7 @@ namespace Barotrauma if (GameMain.Config.Language.Equals(subElement.GetAttributeString("language", ""), StringComparison.OrdinalIgnoreCase)) { uint overrideFontSize = GetFontSize(subElement, 0); - if (overrideFontSize > 0) { return overrideFontSize; } + if (overrideFontSize > 0) { return (uint)Math.Round(overrideFontSize * GameSettings.TextScale); } } } @@ -354,10 +358,10 @@ namespace Barotrauma Point maxResolution = subElement.GetAttributePoint("maxresolution", new Point(int.MaxValue, int.MaxValue)); if (GameMain.GraphicsWidth <= maxResolution.X && GameMain.GraphicsHeight <= maxResolution.Y) { - return (uint)subElement.GetAttributeInt("size", 14); + return (uint)Math.Round(subElement.GetAttributeInt("size", 14) * GameSettings.TextScale); } } - return defaultSize; + return (uint)Math.Round(defaultSize * GameSettings.TextScale); } private string GetFontFilePath(XElement element) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs index e1cc01fa6..523dc49b9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs @@ -240,20 +240,20 @@ namespace Barotrauma public class StrikethroughSettings { - private Color color = GUI.Style.Red; + public Color Color { get; set; } = GUI.Style.Red; private int thickness; private int expand; public StrikethroughSettings(Color? color = null, int thickness = 1, int expand = 0) { - if (color != null) this.color = color.Value; + if (color != null) { Color = color.Value; } this.thickness = thickness; this.expand = expand; } public void Draw(SpriteBatch spriteBatch, float textSizeHalf, float xPos, float yPos) { - ShapeExtensions.DrawLine(spriteBatch, new Vector2(xPos - textSizeHalf - expand, yPos), new Vector2(xPos + textSizeHalf + expand, yPos), color, thickness); + ShapeExtensions.DrawLine(spriteBatch, new Vector2(xPos - textSizeHalf - expand, yPos), new Vector2(xPos + textSizeHalf + expand, yPos), Color, thickness); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs index cf2af2c55..3c709beab 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs @@ -251,7 +251,7 @@ namespace Barotrauma public bool Readonly { get; set; } public GUITextBox(RectTransform rectT, string text = "", Color? textColor = null, ScalableFont font = null, - Alignment textAlignment = Alignment.Left, bool wrap = false, string style = "", Color? color = null, bool createClearButton = false) + Alignment textAlignment = Alignment.Left, bool wrap = false, string style = "", Color? color = null, bool createClearButton = false, bool createPenIcon = true) : base(style, rectT) { HoverCursor = CursorState.IBeam; @@ -283,7 +283,7 @@ namespace Barotrauma clearButtonWidth = (int)(clearButton.Rect.Width * 1.2f); } - if (this.style != null && this.style.ChildStyles.ContainsKey("textboxicon")) + if (this.style != null && this.style.ChildStyles.ContainsKey("textboxicon") && createPenIcon) { icon = new GUIImage(new RectTransform(new Vector2(0.6f, 0.6f), frame.RectTransform, Anchor.CenterRight, scaleBasis: ScaleBasis.BothHeight) { AbsoluteOffset = new Point(5 + clearButtonWidth, 0) }, null, scaleToFit: true); icon.ApplyStyle(this.style.ChildStyles["textboxicon"]); @@ -457,6 +457,11 @@ namespace Barotrauma isSelecting = PlayerInput.KeyDown(Keys.LeftShift) || PlayerInput.KeyDown(Keys.RightShift); } + if (mouseHeldInside && !PlayerInput.PrimaryMouseButtonHeld()) + { + mouseHeldInside = false; + } + if (CaretEnabled) { if (textBlock.OverflowClipActive) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs index db8735c88..b1aed13d8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -24,9 +24,17 @@ namespace Barotrauma private int buyTotal, sellTotal; private GUITextBlock merchantBalanceBlock; + private GUILayoutGroup valueChangeGroup; + private GUITextBlock currentSellValueBlock, newSellValueBlock; + private GUIImage sellValueChangeArrow; private GUIDropDown sortingDropDown; private GUITextBox searchBox; - private GUIListBox storeDealsList, storeBuyList, storeSellList; + private GUIListBox storeBuyList, storeSellList; + /// + /// Can be null when there are no deals at the current location + /// + private GUILayoutGroup storeDailySpecialsGroup, storeRequestedGoodGroup; + private Color storeSpecialColor; private GUIListBox shoppingCrateBuyList, shoppingCrateSellList; private GUITextBlock shoppingCrateTotal; @@ -49,7 +57,6 @@ namespace Barotrauma private enum StoreTab { - Deals, Buy, Sell } @@ -78,19 +85,19 @@ namespace Barotrauma CurrentLocation.Reputation.OnReputationValueChanged += () => { needsRefresh = true; }; } campaignUI.Campaign.CargoManager.OnItemsInBuyCrateChanged += () => { needsBuyingRefresh = true; }; - campaignUI.Campaign.CargoManager.OnPurchasedItemsChanged += () => { needsBuyingRefresh = true; }; + campaignUI.Campaign.CargoManager.OnPurchasedItemsChanged += () => { needsRefresh = true; }; campaignUI.Campaign.CargoManager.OnItemsInSellCrateChanged += () => { needsSellingRefresh = true; }; campaignUI.Campaign.CargoManager.OnSoldItemsChanged += () => { needsItemsToSellRefresh = true; - needsSellingRefresh = true; + needsRefresh = true; }; } - public void Refresh() + public void Refresh(bool updateOwned = true) { hadPermissions = HasPermissions; - UpdateOwnedItems(); + if (updateOwned) { UpdateOwnedItems(); } RefreshBuying(updateOwned: false); RefreshSelling(updateOwned: false); needsRefresh = false; @@ -100,10 +107,8 @@ namespace Barotrauma { if (updateOwned) { UpdateOwnedItems(); } RefreshShoppingCrateBuyList(); - //RefreshStoreDealsList(); RefreshStoreBuyList(); var hasPermissions = HasPermissions; - //storeDealsList.Enabled = hasPermissions; storeBuyList.Enabled = hasPermissions; shoppingCrateBuyList.Enabled = hasPermissions; needsBuyingRefresh = false; @@ -166,7 +171,12 @@ namespace Barotrauma }; // Merchant balance ------------------------------------------------ - var merchantBalanceContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.75f / 14.0f), storeContent.RectTransform)) + var balanceAndValueGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.75f / 14.0f), storeContent.RectTransform), isHorizontal: true) + { + RelativeSpacing = 0.005f + }; + + var merchantBalanceContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), balanceAndValueGroup.RectTransform)) { RelativeSpacing = 0.005f }; @@ -177,29 +187,111 @@ namespace Barotrauma ForceUpperCase = true }; merchantBalanceBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), merchantBalanceContainer.RectTransform), - "", font: GUI.SubHeadingFont, textAlignment: Alignment.TopLeft) + "", font: GUI.SubHeadingFont) { AutoScaleVertical = true, TextScale = 1.1f, TextGetter = () => { - var balance = CurrentLocation != null ? CurrentLocation.StoreCurrentBalance : 0; - if (balance < (int)(0.25f * Location.StoreInitialBalance)) + if (CurrentLocation != null) { - merchantBalanceBlock.TextColor = Color.Red; - } - else if (balance < (int)(0.5f * Location.StoreInitialBalance)) - { - merchantBalanceBlock.TextColor = Color.Orange; + merchantBalanceBlock.TextColor = CurrentLocation.BalanceColor; + return GetCurrencyFormatted(CurrentLocation.StoreCurrentBalance); } else { - merchantBalanceBlock.TextColor = Color.White; + merchantBalanceBlock.TextColor = Color.Red; + return GetCurrencyFormatted(0); } - return GetCurrencyFormatted(balance); } }; + // Item sell value ------------------------------------------------ + var sellValueContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), balanceAndValueGroup.RectTransform)) + { + CanBeFocused = false, + RelativeSpacing = 0.005f + }; + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), sellValueContainer.RectTransform), + TextManager.Get("campaignstore.sellvalue"), font: GUI.Font, textAlignment: Alignment.BottomLeft) + { + AutoScaleVertical = true, + CanBeFocused = false, + ForceUpperCase = true + }; + + valueChangeGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), sellValueContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + CanBeFocused = true, + RelativeSpacing = 0.02f + }; + float blockWidth = GUI.IsFourByThree() ? 0.32f : 0.28f; + Point blockMaxSize = new Point((int)(GameSettings.TextScale * 60), valueChangeGroup.Rect.Height); + currentSellValueBlock = new GUITextBlock(new RectTransform(new Vector2(blockWidth, 1.0f), valueChangeGroup.RectTransform) { MaxSize = blockMaxSize }, + "", font: GUI.SubHeadingFont) + { + AutoScaleVertical = true, + CanBeFocused = false, + TextScale = 1.1f, + TextGetter = () => + { + if (CurrentLocation != null) + { + int balanceAfterTransaction = IsBuying ? + CurrentLocation.StoreCurrentBalance + buyTotal : + CurrentLocation.StoreCurrentBalance - sellTotal; + if (balanceAfterTransaction != CurrentLocation.StoreCurrentBalance) + { + var newStatus = Location.GetStoreBalanceStatus(balanceAfterTransaction); + if (CurrentLocation.ActiveStoreBalanceStatus.SellPriceModifier != newStatus.SellPriceModifier) + { + string tooltipTag = newStatus.SellPriceModifier > CurrentLocation.ActiveStoreBalanceStatus.SellPriceModifier ? + "campaingstore.valueincreasetooltip" : "campaingstore.valuedecreasetooltip"; + valueChangeGroup.ToolTip = TextManager.Get(tooltipTag); + currentSellValueBlock.TextColor = newStatus.Color; + sellValueChangeArrow.Color = newStatus.Color; + sellValueChangeArrow.Visible = true; + newSellValueBlock.TextColor = newStatus.Color; + newSellValueBlock.Text = $"{(newStatus.SellPriceModifier * 100).FormatZeroDecimal()} %"; + return $"{(CurrentLocation.ActiveStoreBalanceStatus.SellPriceModifier * 100).FormatZeroDecimal()} %"; + } + } + valueChangeGroup.ToolTip = null; + currentSellValueBlock.TextColor = CurrentLocation.BalanceColor; + sellValueChangeArrow.Visible = false; + newSellValueBlock.Text = null; + return $"{(CurrentLocation.ActiveStoreBalanceStatus.SellPriceModifier * 100).FormatZeroDecimal()} %"; + } + else + { + valueChangeGroup.ToolTip = null; + sellValueChangeArrow.Visible = false; + newSellValueBlock.Text = null; + return null; + } + } + }; + Vector4 newPadding = currentSellValueBlock.Padding; + newPadding.Z = 0; + currentSellValueBlock.Padding = newPadding; + float relativeHeight = 0.45f; + float relativeWidth = (relativeHeight * valueChangeGroup.Rect.Height) / valueChangeGroup.Rect.Width; + sellValueChangeArrow = new GUIImage(new RectTransform(new Vector2(relativeWidth, relativeHeight), valueChangeGroup.RectTransform), "StoreArrow", scaleToFit: true) + { + CanBeFocused = false, + Visible = false + }; + newSellValueBlock = new GUITextBlock(new RectTransform(new Vector2(blockWidth, 1.0f), valueChangeGroup.RectTransform) { MaxSize = blockMaxSize }, + "", font: GUI.SubHeadingFont) + { + AutoScaleVertical = true, + CanBeFocused = false, + TextScale = 1.1f + }; + newPadding = newSellValueBlock.Padding; + newPadding.X = 0; + newSellValueBlock.Padding = newPadding; + // Store mode buttons ------------------------------------------------ var modeButtonFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.6f / 14.0f), storeContent.RectTransform), style: null); var modeButtonContainer = new GUILayoutGroup(new RectTransform(Vector2.One, modeButtonFrame.RectTransform), isHorizontal: true); @@ -209,8 +301,6 @@ namespace Barotrauma tabSortingMethods.Clear(); foreach (StoreTab tab in tabs) { - // TODO: Remove the row below once the deal page is implemented - if (tab == StoreTab.Deals) { continue; } var tabButton = new GUIButton(new RectTransform(new Vector2(1.0f / (tabs.Length + 1), 1.0f), modeButtonContainer.RectTransform), text: TextManager.Get("campaignstoretab." + tab), style: "GUITabButton") { @@ -309,24 +399,22 @@ namespace Barotrauma searchBox.OnTextChanged += (textBox, text) => { FilterStoreItems(null, text); return true; }; var storeItemListContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.92f), sortFilterListContainer.RectTransform), style: null); - storeDealsList = new GUIListBox(new RectTransform(Vector2.One, storeItemListContainer.RectTransform)) - { - AutoHideScrollBar = false, - Visible = false - }; tabLists.Clear(); - tabLists.Add(StoreTab.Deals, storeDealsList); + storeBuyList = new GUIListBox(new RectTransform(Vector2.One, storeItemListContainer.RectTransform)) { AutoHideScrollBar = false, Visible = false }; + storeDailySpecialsGroup = CreateDealsGroup(storeBuyList); tabLists.Add(StoreTab.Buy, storeBuyList); + storeSellList = new GUIListBox(new RectTransform(Vector2.One, storeItemListContainer.RectTransform)) { AutoHideScrollBar = false, Visible = false }; + storeRequestedGoodGroup = CreateDealsGroup(storeSellList); tabLists.Add(StoreTab.Sell, storeSellList); // Shopping Crate ------------------------------------------------------------------------------------------------------------------------------------------ @@ -428,6 +516,26 @@ namespace Barotrauma resolutionWhenCreated = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); } + private GUILayoutGroup CreateDealsGroup(GUIListBox parentList) + { + var elementHeight = (int)(GUI.yScale * 80); + var frame = new GUIFrame(new RectTransform(new Point(parentList.Content.Rect.Width, 4 * elementHeight + 3), parent: parentList.Content.RectTransform), style: null); + frame.UserData = "deals"; + var dealsGroup = new GUILayoutGroup(new RectTransform(Vector2.One, frame.RectTransform, anchor: Anchor.Center), childAnchor: Anchor.TopCenter); + var dealsHeader = new GUILayoutGroup(new RectTransform(new Point((int)(0.95f * parentList.Content.Rect.Width), elementHeight), parent: dealsGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + dealsHeader.UserData = "header"; + var iconWidth = (0.9f * dealsHeader.Rect.Height) / dealsHeader.Rect.Width; + var dealsIcon = new GUIImage(new RectTransform(new Vector2(iconWidth, 0.9f), dealsHeader.RectTransform), "StoreDealIcon", scaleToFit: true); + var text = TextManager.Get(parentList == storeBuyList ? "campaignstore.dailyspecials" : "campaignstore.requestedgoods"); + var dealsText = new GUITextBlock(new RectTransform(new Vector2(1.0f - iconWidth, 0.9f), dealsHeader.RectTransform), text, font: GUI.LargeFont); + storeSpecialColor = dealsIcon.Color; + dealsText.TextColor = storeSpecialColor; + var divider = new GUIImage(new RectTransform(new Point(dealsGroup.Rect.Width, 3), dealsGroup.RectTransform), "HorizontalLine"); + divider.UserData = "divider"; + frame.CanBeFocused = dealsGroup.CanBeFocused = dealsHeader.CanBeFocused = dealsIcon.CanBeFocused = dealsText.CanBeFocused = divider.CanBeFocused = false; + return dealsGroup; + } + private void UpdateLocation(Location prevLocation, Location newLocation) { if (prevLocation == newLocation) { return; } @@ -464,17 +572,8 @@ namespace Barotrauma SetConfirmButtonBehavior(); SetConfirmButtonStatus(); FilterStoreItems(); - if (tab == StoreTab.Deals) + if (tab == StoreTab.Buy) { - storeBuyList.Visible = false; - storeSellList.Visible = false; - storeDealsList.Visible = true; - shoppingCrateSellList.Visible = false; - shoppingCrateBuyList.Visible = true; - } - else if (tab == StoreTab.Buy) - { - storeDealsList.Visible = false; storeSellList.Visible = false; storeBuyList.Visible = true; shoppingCrateSellList.Visible = false; @@ -482,7 +581,6 @@ namespace Barotrauma } else if (tab == StoreTab.Sell) { - storeDealsList.Visible = false; storeBuyList.Visible = false; storeSellList.Visible = true; shoppingCrateBuyList.Visible = false; @@ -525,37 +623,71 @@ namespace Barotrauma bool hasPermissions = HasPermissions; HashSet existingItemFrames = new HashSet(); + + if ((storeDailySpecialsGroup != null) != CurrentLocation.DailySpecials.Any()) + { + if (storeDailySpecialsGroup == null) + { + storeDailySpecialsGroup = CreateDealsGroup(storeBuyList); + storeDailySpecialsGroup.Parent.SetAsFirstChild(); + } + else + { + storeBuyList.RemoveChild(storeDailySpecialsGroup.Parent); + storeDailySpecialsGroup = null; + } + storeBuyList.RecalculateChildren(); + } + foreach (PurchasedItem item in CurrentLocation.StoreStock) { - if (item.ItemPrefab.CanBeBoughtAtLocation(CurrentLocation, out PriceInfo priceInfo)) + CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity); + } + + foreach (ItemPrefab itemPrefab in CurrentLocation.DailySpecials) + { + if (CurrentLocation.StoreStock.Any(pi => pi.ItemPrefab == itemPrefab)) { continue; } + CreateOrUpdateItemFrame(itemPrefab, 0); + } + + void CreateOrUpdateItemFrame(ItemPrefab itemPrefab, int quantity) + { + if (itemPrefab.CanBeBoughtAtLocation(CurrentLocation, out PriceInfo priceInfo)) { - var itemFrame = storeBuyList.Content.Children.FirstOrDefault(c => c.UserData is PurchasedItem pi && pi.ItemPrefab == item.ItemPrefab); - var quantity = item.Quantity; - if (CargoManager.PurchasedItems.Find(i => i.ItemPrefab == item.ItemPrefab) is PurchasedItem purchasedItem) + var isDailySpecial = CurrentLocation.DailySpecials.Contains(itemPrefab); + var itemFrame = isDailySpecial ? + storeDailySpecialsGroup.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab == itemPrefab) : + storeBuyList.Content.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab == itemPrefab); + if (CargoManager.PurchasedItems.Find(i => i.ItemPrefab == itemPrefab) is PurchasedItem purchasedItem) { quantity = Math.Max(quantity - purchasedItem.Quantity, 0); } - if (CargoManager.ItemsInBuyCrate.Find(i => i.ItemPrefab == item.ItemPrefab) is PurchasedItem itemInBuyCrate) + if (CargoManager.ItemsInBuyCrate.Find(i => i.ItemPrefab == itemPrefab) is PurchasedItem itemInBuyCrate) { quantity = Math.Max(quantity - itemInBuyCrate.Quantity, 0); } if (itemFrame == null) { - itemFrame = CreateItemFrame(new PurchasedItem(item.ItemPrefab, quantity), priceInfo, storeBuyList, forceDisable: !hasPermissions); + var parentComponent = isDailySpecial ? storeDailySpecialsGroup : storeBuyList as GUIComponent; + itemFrame = CreateItemFrame(new PurchasedItem(itemPrefab, quantity), priceInfo, parentComponent, forceDisable: !hasPermissions); } else { (itemFrame.UserData as PurchasedItem).Quantity = quantity; SetQuantityLabelText(StoreTab.Buy, itemFrame); SetOwnedLabelText(itemFrame); - SetItemFrameStatus(itemFrame, hasPermissions && quantity > 0); } + SetItemFrameStatus(itemFrame, hasPermissions && quantity > 0); existingItemFrames.Add(itemFrame); } } - var removedItemFrames = storeBuyList.Content.Children.Except(existingItemFrames).ToList(); - removedItemFrames.ForEach(f => storeBuyList.Content.RemoveChild(f)); + var removedItemFrames = storeBuyList.Content.Children.Where(c => c.UserData is PurchasedItem).Except(existingItemFrames).ToList(); + if (storeDailySpecialsGroup != null) + { + removedItemFrames.AddRange(storeDailySpecialsGroup.Children.Where(c => c.UserData is PurchasedItem).Except(existingItemFrames).ToList()); + } + removedItemFrames.ForEach(f => f.RectTransform.Parent = null); if (IsBuying) { FilterStoreItems(); } SortItems(StoreTab.Buy); @@ -567,36 +699,72 @@ namespace Barotrauma { float prevSellListScroll = storeSellList.BarScroll; float prevShoppingCrateScroll = shoppingCrateSellList.BarScroll; - bool hasPermissions = HasPermissions; HashSet existingItemFrames = new HashSet(); - foreach (PurchasedItem item in itemsToSell) + + if ((storeRequestedGoodGroup != null) != CurrentLocation.RequestedGoods.Any()) { - PriceInfo priceInfo = item.ItemPrefab.GetPriceInfo(CurrentLocation); - if (priceInfo == null) { continue; } - var itemFrame = storeSellList.Content.FindChild(c => c.UserData is PurchasedItem i && i.ItemPrefab == item.ItemPrefab); - var quantity = item.Quantity; - if (CargoManager.ItemsInSellCrate.Find(i => i.ItemPrefab == item.ItemPrefab) is PurchasedItem itemInSellCrate) + if (storeRequestedGoodGroup == null) { - quantity = Math.Max(quantity - itemInSellCrate.Quantity, 0); - } - if (itemFrame == null) - { - itemFrame = CreateItemFrame(new PurchasedItem(item.ItemPrefab, quantity), priceInfo, storeSellList, forceDisable: !hasPermissions); + storeRequestedGoodGroup = CreateDealsGroup(storeSellList); + storeRequestedGoodGroup.Parent.SetAsFirstChild(); } else { - (itemFrame.UserData as PurchasedItem).Quantity = quantity; + storeSellList.RemoveChild(storeRequestedGoodGroup.Parent); + storeRequestedGoodGroup = null; + } + storeSellList.RecalculateChildren(); + } + + foreach (PurchasedItem item in itemsToSell) + { + CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity); + } + + foreach (var requestedGood in CurrentLocation.RequestedGoods) + { + if (itemsToSell.Any(pi => pi.ItemPrefab == requestedGood)) { continue; } + CreateOrUpdateItemFrame(requestedGood, 0); + } + + void CreateOrUpdateItemFrame(ItemPrefab itemPrefab, int itemQuantity) + { + PriceInfo priceInfo = itemPrefab.GetPriceInfo(CurrentLocation); + if (priceInfo == null) { return; } + var isRequestedGood = CurrentLocation.RequestedGoods.Contains(itemPrefab); + var itemFrame = isRequestedGood ? + storeRequestedGoodGroup.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab == itemPrefab) : + storeSellList.Content.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab == itemPrefab); + if (CargoManager.ItemsInSellCrate.Find(i => i.ItemPrefab == itemPrefab) is PurchasedItem itemInSellCrate) + { + itemQuantity = Math.Max(itemQuantity - itemInSellCrate.Quantity, 0); + } + if (itemFrame == null) + { + var parentComponent = isRequestedGood ? storeRequestedGoodGroup : storeSellList as GUIComponent; + itemFrame = CreateItemFrame(new PurchasedItem(itemPrefab, itemQuantity), priceInfo, parentComponent, forceDisable: !hasPermissions); + } + else + { + (itemFrame.UserData as PurchasedItem).Quantity = itemQuantity; SetQuantityLabelText(StoreTab.Sell, itemFrame); SetOwnedLabelText(itemFrame); - SetItemFrameStatus(itemFrame, hasPermissions); } - if (quantity < 1) { itemFrame.Visible = false; } + SetItemFrameStatus(itemFrame, hasPermissions && itemQuantity > 0); + if (itemQuantity < 1 && !isRequestedGood) + { + itemFrame.Visible = false; + } existingItemFrames.Add(itemFrame); } - var removedItemFrames = storeSellList.Content.Children.Except(existingItemFrames).ToList(); - removedItemFrames.ForEach(f => storeSellList.Content.RemoveChild(f)); + var removedItemFrames = storeSellList.Content.Children.Where(c => c.UserData is PurchasedItem).Except(existingItemFrames).ToList(); + if (storeRequestedGoodGroup != null) + { + removedItemFrames.AddRange(storeRequestedGoodGroup.Children.Where(c => c.UserData is PurchasedItem).Except(existingItemFrames).ToList()); + } + removedItemFrames.ForEach(f => f.RectTransform.Parent = null); if (IsSelling) { FilterStoreItems(); } SortItems(StoreTab.Sell); @@ -673,13 +841,10 @@ namespace Barotrauma } suppressBuySell = false; - if (priceInfo != null) - { - var price = listBox == shoppingCrateBuyList ? - CurrentLocation.GetAdjustedItemBuyPrice(priceInfo) : - CurrentLocation.GetAdjustedItemSellPrice(priceInfo); - totalPrice += item.Quantity * price; - } + var price = listBox == shoppingCrateBuyList ? + CurrentLocation.GetAdjustedItemBuyPrice(item.ItemPrefab, priceInfo: priceInfo) : + CurrentLocation.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo); + totalPrice += item.Quantity * price; } var removedItemFrames = listBox.Content.Children.Except(existingItemFrames).ToList(); @@ -711,32 +876,138 @@ namespace Barotrauma if (sortingMethod == SortingMethod.AlphabeticalAsc || sortingMethod == SortingMethod.AlphabeticalDesc) { - list.Content.RectTransform.SortChildren( - (x, y) => (x.GUIComponent.UserData as PurchasedItem).ItemPrefab.Name.CompareTo((y.GUIComponent.UserData as PurchasedItem).ItemPrefab.Name)); - if (sortingMethod == SortingMethod.AlphabeticalDesc) { list.Content.RectTransform.ReverseChildren(); } + list.Content.RectTransform.SortChildren(CompareByName); + if (GetSpecialsGroup() is GUILayoutGroup specialsGroup) + { + specialsGroup.RectTransform.SortChildren(CompareByName); + specialsGroup.Recalculate(); + } + + int CompareByName(RectTransform x, RectTransform y) + { + if (x.GUIComponent.UserData is PurchasedItem itemX && y.GUIComponent.UserData is PurchasedItem itemY) + { + var sortResult = itemX.ItemPrefab.Name.CompareTo(itemY.ItemPrefab.Name); + if (sortingMethod == SortingMethod.AlphabeticalDesc) { sortResult *= -1; } + return sortResult; + } + else + { + return CompareByElement(x, y); + } + } } else if (sortingMethod == SortingMethod.PriceAsc || sortingMethod == SortingMethod.PriceDesc) { SortItems(list, SortingMethod.AlphabeticalAsc); if (list == storeSellList || list == shoppingCrateSellList) { - list.Content.RectTransform.SortChildren( - (x, y) => CurrentLocation.GetAdjustedItemSellPrice((x.GUIComponent.UserData as PurchasedItem).ItemPrefab).CompareTo( - CurrentLocation.GetAdjustedItemSellPrice((y.GUIComponent.UserData as PurchasedItem).ItemPrefab))); + list.Content.RectTransform.SortChildren(CompareBySellPrice); + if (GetSpecialsGroup() is GUILayoutGroup specialsGroup) + { + specialsGroup.RectTransform.SortChildren(CompareBySellPrice); + specialsGroup.Recalculate(); + } + + int CompareBySellPrice(RectTransform x, RectTransform y) + { + if (x.GUIComponent.UserData is PurchasedItem itemX && y.GUIComponent.UserData is PurchasedItem itemY) + { + var sortResult = CurrentLocation.GetAdjustedItemSellPrice(itemX.ItemPrefab).CompareTo( + CurrentLocation.GetAdjustedItemSellPrice(itemY.ItemPrefab)); + if (sortingMethod == SortingMethod.PriceDesc) { sortResult *= -1; } + return sortResult; + } + else + { + return CompareByElement(x, y); + } + } } else { - list.Content.RectTransform.SortChildren( - (x, y) => CurrentLocation.GetAdjustedItemBuyPrice((x.GUIComponent.UserData as PurchasedItem).ItemPrefab).CompareTo( - CurrentLocation.GetAdjustedItemBuyPrice((y.GUIComponent.UserData as PurchasedItem).ItemPrefab))); + list.Content.RectTransform.SortChildren(CompareByBuyPrice); + if (GetSpecialsGroup() is GUILayoutGroup specialsGroup) + { + specialsGroup.RectTransform.SortChildren(CompareByBuyPrice); + specialsGroup.Recalculate(); + } + + int CompareByBuyPrice(RectTransform x, RectTransform y) + { + if (x.GUIComponent.UserData is PurchasedItem itemX && y.GUIComponent.UserData is PurchasedItem itemY) + { + var sortResult = CurrentLocation.GetAdjustedItemBuyPrice(itemX.ItemPrefab).CompareTo( + CurrentLocation.GetAdjustedItemBuyPrice(itemY.ItemPrefab)); + if (sortingMethod == SortingMethod.PriceDesc) { sortResult *= -1; } + return sortResult; + } + else + { + return CompareByElement(x, y); + } + } } - if (sortingMethod == SortingMethod.PriceDesc) { list.Content.RectTransform.ReverseChildren(); } } else if (sortingMethod == SortingMethod.CategoryAsc) { SortItems(list, SortingMethod.AlphabeticalAsc); - list.Content.RectTransform.SortChildren((x, y) => - (x.GUIComponent.UserData as PurchasedItem).ItemPrefab.Category.CompareTo((y.GUIComponent.UserData as PurchasedItem).ItemPrefab.Category)); + list.Content.RectTransform.SortChildren(CompareByCategory); + if (GetSpecialsGroup() is GUILayoutGroup specialsGroup) + { + specialsGroup.RectTransform.SortChildren(CompareByCategory); + specialsGroup.Recalculate(); + } + + static int CompareByCategory(RectTransform x, RectTransform y) + { + if (x.GUIComponent.UserData is PurchasedItem itemX && y.GUIComponent.UserData is PurchasedItem itemY) + { + return itemX.ItemPrefab.Category.CompareTo(itemY.ItemPrefab.Category); + } + else + { + return CompareByElement(x, y); + } + } + } + + GUILayoutGroup GetSpecialsGroup() + { + if (list == storeBuyList) + { + return storeDailySpecialsGroup; + } + else if (list == storeSellList) + { + return storeRequestedGoodGroup; + } + else + { + return null; + } + } + + static int CompareByElement(RectTransform x, RectTransform y) + { + if (ShouldBeOnTop(x) || ShouldBeOnBottom(y)) + { + return -1; + } + else if (ShouldBeOnBottom(x) || ShouldBeOnTop(y)) + { + return 1; + } + else + { + return 0; + } + + static bool ShouldBeOnTop(RectTransform rt) => + rt.GUIComponent.UserData is string id && (id == "deals" || id == "header"); + + static bool ShouldBeOnBottom(RectTransform rt) => + rt.GUIComponent.UserData is string id && id == "divider"; } } @@ -750,7 +1021,7 @@ namespace Barotrauma private void SortActiveTabItems(SortingMethod sortingMethod) => SortItems(activeTab, sortingMethod); - private GUIComponent CreateItemFrame(PurchasedItem pi, PriceInfo priceInfo, GUIListBox listBox, bool forceDisable = false) + private GUIComponent CreateItemFrame(PurchasedItem pi, PriceInfo priceInfo, GUIComponent parentComponent, bool forceDisable = false) { var tooltip = pi.ItemPrefab.Name; if (!string.IsNullOrWhiteSpace(pi.ItemPrefab.Description)) @@ -758,7 +1029,21 @@ namespace Barotrauma tooltip += "\n" + pi.ItemPrefab.Description; } - GUIFrame frame = new GUIFrame(new RectTransform(new Point(listBox.Content.Rect.Width, (int)(GUI.yScale * 80)), parent: listBox.Content.RectTransform), style: "ListBoxElement") + GUIListBox parentListBox = parentComponent as GUIListBox; + int width = 0; + RectTransform parent = null; + if (parentListBox != null) + { + width = parentListBox.Content.Rect.Width; + parent = parentListBox.Content.RectTransform; + } + else + { + width = parentComponent.Rect.Width; + parent = parentComponent.RectTransform; + } + + GUIFrame frame = new GUIFrame(new RectTransform(new Point(width, (int)(GUI.yScale * 80)), parent: parent), style: "ListBoxElement") { ToolTip = tooltip, UserData = pi @@ -788,33 +1073,58 @@ namespace Barotrauma img.RectTransform.MaxSize = img.Rect.Size; } - GUILayoutGroup nameAndQuantityGroup = new GUILayoutGroup(new RectTransform(new Vector2(nameAndIconRelativeWidth - iconRelativeWidth, 1.0f), mainGroup.RectTransform)) + GUIFrame nameAndQuantityFrame = new GUIFrame(new RectTransform(new Vector2(nameAndIconRelativeWidth - iconRelativeWidth, 1.0f), mainGroup.RectTransform), style: null) + { + CanBeFocused = false + }; + GUILayoutGroup nameAndQuantityGroup = new GUILayoutGroup(new RectTransform(Vector2.One, nameAndQuantityFrame.RectTransform)) { CanBeFocused = false, Stretch = true }; + var isSellingRelatedList = parentComponent == storeSellList || parentComponent == storeRequestedGoodGroup || parentComponent == shoppingCrateSellList; + var locationHasDealOnItem = isSellingRelatedList ? + CurrentLocation.RequestedGoods.Contains(pi.ItemPrefab) : CurrentLocation.DailySpecials.Contains(pi.ItemPrefab); GUITextBlock nameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), nameAndQuantityGroup.RectTransform), pi.ItemPrefab.Name, font: GUI.SubHeadingFont, textAlignment: Alignment.BottomLeft) { CanBeFocused = false, + Shadow = locationHasDealOnItem, TextColor = Color.White * (forceDisable ? 0.5f : 1.0f), TextScale = 0.85f, UserData = "name" }; + if (locationHasDealOnItem) + { + var relativeWidth = (0.9f * nameAndQuantityFrame.Rect.Height) / nameAndQuantityFrame.Rect.Width; + var dealIcon = new GUIImage( + new RectTransform(new Vector2(relativeWidth, 0.9f), nameAndQuantityFrame.RectTransform, anchor: Anchor.CenterLeft) + { + AbsoluteOffset = new Point((int)nameBlock.Padding.X, 0) + }, + "StoreDealIcon", scaleToFit: true) + { + CanBeFocused = false + }; + dealIcon.SetAsFirstChild(); + } + var isParentOnLeftSideOfInterface = parentComponent == storeBuyList || parentComponent == storeDailySpecialsGroup || + parentComponent == storeSellList || parentComponent == storeRequestedGoodGroup; GUILayoutGroup shoppingCrateAmountGroup = null; GUINumberInput amountInput = null; - if (listBox == storeBuyList || listBox == storeSellList) + if (isParentOnLeftSideOfInterface) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), nameAndQuantityGroup.RectTransform), - CreateQuantityLabelText(listBox == storeSellList ? StoreTab.Sell : StoreTab.Buy, pi.Quantity), font: GUI.Font, textAlignment: Alignment.BottomLeft) + CreateQuantityLabelText(isSellingRelatedList ? StoreTab.Sell : StoreTab.Buy, pi.Quantity), font: GUI.Font, textAlignment: Alignment.BottomLeft) { CanBeFocused = false, + Shadow = locationHasDealOnItem, TextColor = Color.White * (forceDisable ? 0.5f : 1.0f), TextScale = 0.85f, UserData = "quantitylabel" }; } - else if (listBox == shoppingCrateBuyList || listBox == shoppingCrateSellList) + else if (!isParentOnLeftSideOfInterface) { var relativePadding = nameBlock.Padding.X / nameBlock.Rect.Width; shoppingCrateAmountGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f - relativePadding, 0.6f), nameAndQuantityGroup.RectTransform) { RelativeOffset = new Vector2(relativePadding, 0) }, @@ -825,7 +1135,7 @@ namespace Barotrauma amountInput = new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), shoppingCrateAmountGroup.RectTransform), GUINumberInput.NumberType.Int) { MinValueInt = 0, - MaxValueInt = GetMaxAvailable(pi.ItemPrefab, listBox == shoppingCrateBuyList ? StoreTab.Buy : StoreTab.Sell), + MaxValueInt = GetMaxAvailable(pi.ItemPrefab, isSellingRelatedList ? StoreTab.Sell : StoreTab.Buy), UserData = pi, IntValue = pi.Quantity }; @@ -856,6 +1166,7 @@ namespace Barotrauma textAlignment: shoppingCrateAmountGroup == null ? Alignment.TopLeft : Alignment.CenterLeft) { CanBeFocused = false, + Shadow = locationHasDealOnItem, TextColor = Color.White * (forceDisable ? 0.5f : 1.0f), TextScale = 0.85f, UserData = "owned" @@ -864,22 +1175,53 @@ namespace Barotrauma var buttonRelativeWidth = (0.9f * mainGroup.Rect.Height) / mainGroup.Rect.Width; - var priceBlock = new GUITextBlock(new RectTransform(new Vector2(priceAndButtonRelativeWidth - buttonRelativeWidth, 1.0f), mainGroup.RectTransform), "", font: GUI.SubHeadingFont, textAlignment: Alignment.Right) + var priceFrame = new GUIFrame(new RectTransform(new Vector2(priceAndButtonRelativeWidth - buttonRelativeWidth, 1.0f), mainGroup.RectTransform), style: null) + { + CanBeFocused = false + }; + var priceBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), priceFrame.RectTransform, anchor: Anchor.Center), + "0 MK", font: GUI.SubHeadingFont, textAlignment: Alignment.Right) { CanBeFocused = false, - TextColor = Color.White * (forceDisable ? 0.5f : 1.0f), + TextColor = locationHasDealOnItem ? storeSpecialColor : Color.White, UserData = "price" }; - if (listBox == storeSellList || listBox == shoppingCrateSellList) + priceBlock.Color *= (forceDisable ? 0.5f : 1.0f); + priceBlock.CalculateHeightFromText(); + if (isSellingRelatedList) { - priceBlock.TextGetter = () => GetCurrencyFormatted(CurrentLocation?.GetAdjustedItemSellPrice(priceInfo) ?? 0); + priceBlock.TextGetter = () => GetCurrencyFormatted(CurrentLocation?.GetAdjustedItemSellPrice(pi.ItemPrefab, priceInfo: priceInfo) ?? 0); } else { - priceBlock.TextGetter = () => GetCurrencyFormatted(CurrentLocation?.GetAdjustedItemBuyPrice(priceInfo) ?? 0); + priceBlock.TextGetter = () => GetCurrencyFormatted(CurrentLocation?.GetAdjustedItemBuyPrice(pi.ItemPrefab, priceInfo: priceInfo) ?? 0); + } + if (locationHasDealOnItem) + { + var undiscounterPriceBlock = new GUITextBlock( + new RectTransform(new Vector2(1.0f, 0.25f), priceFrame.RectTransform, anchor: Anchor.Center) + { + AbsoluteOffset = new Point(0, priceBlock.RectTransform.ScaledSize.Y) + }, "", font: GUI.SmallFont, textAlignment: Alignment.Center) + { + CanBeFocused = false, + Strikethrough = new GUITextBlock.StrikethroughSettings(color: priceBlock.TextColor, expand: 1), + TextColor = priceBlock.TextColor, + UserData = "undiscountedprice" + }; + if (isSellingRelatedList) + { + undiscounterPriceBlock.TextGetter = () => GetCurrencyFormatted( + CurrentLocation?.GetAdjustedItemSellPrice(pi.ItemPrefab, priceInfo: priceInfo, considerRequestedGoods: false) ?? 0); + } + else + { + undiscounterPriceBlock.TextGetter = () => GetCurrencyFormatted( + CurrentLocation?.GetAdjustedItemBuyPrice(pi.ItemPrefab, priceInfo: priceInfo, considerDailySpecials: false) ?? 0); + } } - if (listBox == storeDealsList || listBox == storeBuyList || listBox == storeSellList) + if (isParentOnLeftSideOfInterface) { new GUIButton(new RectTransform(new Vector2(buttonRelativeWidth, 0.9f), mainGroup.RectTransform), style: "StoreAddToCrateButton") { @@ -902,7 +1244,14 @@ namespace Barotrauma }; } - listBox.RecalculateChildren(); + if (parentListBox != null) + { + parentListBox.RecalculateChildren(); + } + else if (parentComponent is GUILayoutGroup parentLayoutGroup) + { + parentLayoutGroup.Recalculate(); + } mainGroup.Recalculate(); mainGroup.RectTransform.RecalculateChildren(true, true); amountInput?.LayoutGroup.Recalculate(); @@ -923,15 +1272,20 @@ namespace Barotrauma .ForEach(i => AddToOwnedItems(i.Prefab)); // Add items in character inventories - foreach (Character c in GameMain.GameSession.CrewManager.GetCharacters()) + foreach (var item in Item.ItemList) { - Item.ItemList.Where(i => i != null && i.GetRootInventoryOwner() == c) - .ForEach(i => AddToOwnedItems(i.Prefab)); + if (item == null || item.Removed) { continue; } + var rootInventoryOwner = item.GetRootInventoryOwner(); + var ownedByCrewMember = GameMain.GameSession.CrewManager.GetCharacters().Any(c => c == rootInventoryOwner); + if (!ownedByCrewMember) { continue; } + AddToOwnedItems(item.Prefab); } // Add items already purchased CargoManager?.PurchasedItems?.ForEach(pi => AddToOwnedItems(pi.ItemPrefab, amount: pi.Quantity)); + ownedItemsUpdateTimer = 0.0f; + void AddToOwnedItems(ItemPrefab itemPrefab, int amount = 1) { if (OwnedItems.ContainsKey(itemPrefab)) @@ -977,14 +1331,22 @@ namespace Barotrauma numberInput.Enabled = enabled; } - if (itemFrame.FindChild("owned", recursive: true) is GUITextBlock owned) + if (itemFrame.FindChild("owned", recursive: true) is GUITextBlock ownedBlock) { - owned.TextColor = color; + ownedBlock.TextColor = color; } - if (itemFrame.FindChild("price", recursive: true) is GUITextBlock price) + var isDiscounted = false; + if (itemFrame.FindChild("undiscountedprice", recursive: true) is GUITextBlock undiscountedPriceBlock) { - price.TextColor = color; + undiscountedPriceBlock.TextColor = color; + undiscountedPriceBlock.Strikethrough.Color = color; + isDiscounted = true; + } + + if (itemFrame.FindChild("price", recursive: true) is GUITextBlock priceBlock) + { + priceBlock.TextColor = isDiscounted ? storeSpecialColor * (enabled ? 1.0f : 0.5f) : color; } if (itemFrame.FindChild("addbutton", recursive: true) is GUIButton addButton) @@ -1101,7 +1463,7 @@ namespace Barotrauma itemsToRemove.Add(item); continue; } - totalPrice += item.Quantity * CurrentLocation.GetAdjustedItemBuyPrice(priceInfo); + totalPrice += item.Quantity * CurrentLocation.GetAdjustedItemBuyPrice(item.ItemPrefab, priceInfo: priceInfo); } itemsToRemove.ForEach(i => itemsToPurchase.Remove(i)); @@ -1135,7 +1497,7 @@ namespace Barotrauma } if (item.ItemPrefab.GetPriceInfo(CurrentLocation) is PriceInfo priceInfo) { - totalValue += item.Quantity * CurrentLocation.GetAdjustedItemSellPrice(priceInfo); + totalValue += item.Quantity * CurrentLocation.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo); } else { @@ -1197,16 +1559,38 @@ namespace Barotrauma private void SetClearAllButtonStatus() => clearAllButton.Enabled = HasPermissions && ActiveShoppingCrateList.Content.RectTransform.Children.Any(); - public void Update() + private float ownedItemsUpdateTimer = 0.0f; + private readonly float ownedItemsUpdateInterval = 1.5f; + + public void Update(float deltaTime) { if (GameMain.GraphicsWidth != resolutionWhenCreated.X || GameMain.GraphicsHeight != resolutionWhenCreated.Y) { CreateUI(); - needsRefresh = false; } - if (needsRefresh || hadPermissions != HasPermissions) { Refresh(); } - if (needsBuyingRefresh) { RefreshBuying(); } + else + { + // Update the owned items at short intervals and check if the interface should be refreshed + ownedItemsUpdateTimer += deltaTime; + if (ownedItemsUpdateTimer >= ownedItemsUpdateInterval) + { + var prevOwnedItems = new Dictionary(OwnedItems); + UpdateOwnedItems(); + var refresh = (prevOwnedItems.Count != OwnedItems.Count) || + (prevOwnedItems.Select(kvp => kvp.Value).Sum() != OwnedItems.Select(kvp => kvp.Value).Sum()) || + (OwnedItems.Any(kvp => kvp.Value > 0 && !prevOwnedItems.ContainsKey(kvp.Key)) || + prevOwnedItems.Any(kvp => !OwnedItems.TryGetValue(kvp.Key, out var itemCount) || kvp.Value != itemCount)); + if (refresh) + { + needsItemsToSellRefresh = true; + needsRefresh = true; + } + } + } + if (needsItemsToSellRefresh) { RefreshItemsToSell(); } + if (needsRefresh || hadPermissions != HasPermissions) { Refresh(updateOwned: ownedItemsUpdateTimer > 0.0f); } + if (needsBuyingRefresh) { RefreshBuying(); } if (needsSellingRefresh) { RefreshSelling(); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs index 7f80e758f..836c62458 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs @@ -610,8 +610,8 @@ namespace Barotrauma { if (GameMain.Client == null) { - GameMain.GameSession.SwitchSubmarine(selectedSubmarine, deliveryFee); - GameMain.GameSession.Campaign.UpgradeManager.RefundResetAndReload(selectedSubmarine); + SubmarineInfo newSub = GameMain.GameSession.SwitchSubmarine(selectedSubmarine, deliveryFee); + GameMain.GameSession.Campaign.UpgradeManager.RefundResetAndReload(newSub); RefreshSubmarineDisplay(true); } else @@ -645,8 +645,8 @@ namespace Barotrauma if (GameMain.Client == null) { GameMain.GameSession.PurchaseSubmarine(selectedSubmarine); - GameMain.GameSession.SwitchSubmarine(selectedSubmarine, 0); - GameMain.GameSession.Campaign.UpgradeManager.RefundResetAndReload(selectedSubmarine); + SubmarineInfo newSub = GameMain.GameSession.SwitchSubmarine(selectedSubmarine, 0); + GameMain.GameSession.Campaign.UpgradeManager.RefundResetAndReload(newSub); RefreshSubmarineDisplay(true); } else diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index 6078eb9c0..2e159ce12 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -29,7 +29,7 @@ namespace Barotrauma private float sizeMultiplier = 1f; private IEnumerable crew; - private List teamIDs; + private List teamIDs; private const string inLobbyString = "\u2022 \u2022 \u2022"; public static Color OwnCharacterBGColor = Color.Gold * 0.7f; @@ -281,11 +281,11 @@ namespace Barotrauma // Show own team first when there's more than one team if (teamIDs.Count > 1 && GameMain.Client?.Character != null) { - Character.TeamType ownTeam = GameMain.Client.Character.TeamID; + CharacterTeamType ownTeam = GameMain.Client.Character.TeamID; teamIDs = teamIDs.OrderBy(i => i != ownTeam).ThenBy(i => i).ToList(); } - if (!teamIDs.Any()) teamIDs.Add(Character.TeamType.None); + if (!teamIDs.Any()) { teamIDs.Add(CharacterTeamType.None); } var content = new GUILayoutGroup(new RectTransform(Vector2.One, crewFrame.RectTransform)); @@ -465,15 +465,14 @@ namespace Barotrauma { foreach (Character character in crew.Where(c => c.TeamID == teamIDs[i])) { - if (!(character is AICharacter) && connectedClients.Find(c => c.Character == null && c.Name == character.Name) != null) continue; - CreateMultiPlayerCharacterElement(character, GameMain.Client.ConnectedClients.Find(c => c.Character == character), i); + if (!(character is AICharacter) && connectedClients.Any(c => c.Character == null && c.Name == character.Name)) { continue; } + CreateMultiPlayerCharacterElement(character, GameMain.Client.PreviouslyConnectedClients.FirstOrDefault(c => c.Character == character), i); } } for (int j = 0; j < connectedClients.Count; j++) { Client client = connectedClients[j]; - if (!client.InGame || client.Character == null || client.Character.IsDead) { CreateMultiPlayerClientElement(client); @@ -565,7 +564,7 @@ namespace Barotrauma private int GetTeamIndex(Client client) { - if (teamIDs.Count <= 1) return 0; + if (teamIDs.Count <= 1) { return 0; } if (client.Character != null) { @@ -707,7 +706,7 @@ namespace Barotrauma { GUIComponent paddedFrame; - if (client.Character == null) + if (client.Character?.Info == null) { paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.874f, 0.58f), frame.RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.05f) }) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs index f3d6eb5d1..c64af21de 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs @@ -625,13 +625,19 @@ namespace Barotrauma selectedUpgradeCategoryLayout?.ClearChildren(); GUIFrame frame = new GUIFrame(rectT(1, 0.4f, selectedUpgradeCategoryLayout)); GUIListBox prefabList = new GUIListBox(rectT(0.93f, 0.9f, frame, Anchor.Center)) { UserData = "prefablist" }; + + List entitiesOnSub = null; + if (!category.IsWallUpgrade) + { + entitiesOnSub = submarine.GetItems(true).Where(i => submarine.IsEntityFoundOnThisSub(i, true)).ToList(); + } foreach (UpgradePrefab prefab in prefabs) { - CreateUpgradeEntry(prefab, category, prefabList.Content); + CreateUpgradeEntry(prefab, category, prefabList.Content, entitiesOnSub); } } - private void CreateUpgradeEntry(UpgradePrefab prefab, UpgradeCategory category, GUIComponent parent) + private void CreateUpgradeEntry(UpgradePrefab prefab, UpgradeCategory category, GUIComponent parent, List itemsOnSubmarine) { /* UPGRADE PREFAB ENTRY * |------------------------------------------------------------------| @@ -680,12 +686,14 @@ namespace Barotrauma progressLayout.Recalculate(); buyButtonLayout.Recalculate(); - if (!HasPermission) + if (!HasPermission || itemsOnSubmarine != null && !itemsOnSubmarine.Any(it => category.CanBeApplied(it, prefab))) { prefabFrame.Enabled = false; description.Enabled = false; name.Enabled = false; icon.Color = Color.Gray; + buyButton.Enabled = false; + buyButtonLayout.UserData = null; // prevent UpdateUpgradeEntry() from enabling the button } buyButton.OnClicked += (button, o) => @@ -731,7 +739,7 @@ namespace Barotrauma // include pending upgrades into the tooltip foreach (var (prefab, category, level) in Campaign.UpgradeManager.PendingUpgrades) { - if (entity is Item item && category.CanBeApplied(item) || entity is Structure && category.IsWallUpgrade) + if (entity is Item item && category.CanBeApplied(item, prefab) || entity is Structure && category.IsWallUpgrade) { bool found = false; foreach (GUITextBlock textBlock in upgradeList.Content.Children.Where(c => c is GUITextBlock).Cast()) @@ -786,7 +794,7 @@ namespace Barotrauma foreach (UpgradeCategory category in UpgradeCategory.Categories) { - if (entitiesOnSub.Any(item => category.CanBeApplied(item) && !item.disallowedUpgrades.Contains(category.Identifier))) + if (entitiesOnSub.Any(item => category.CanBeApplied(item, null))) { applicableCategories.Add(category); } @@ -826,7 +834,7 @@ namespace Barotrauma HoveredItem = item; if (PlayerInput.PrimaryMouseButtonClicked() && selectedUpgradTab == UpgradeTab.Upgrade && currentStoreLayout != null) { - ScrollToCategory(data => data.Category.CanBeApplied(item)); + ScrollToCategory(data => data.Category.CanBeApplied(item, null)); } found = true; break; @@ -895,7 +903,7 @@ namespace Barotrauma submarineInfoFrame.RectTransform.ScreenSpaceOffset = new Point(0, (int)(16 * GUI.Scale)); description.Padding = new Vector4(description.Padding.X, 24 * GUI.Scale, description.Padding.Z, description.Padding.W); - List pointsOfInterest = (from category in UpgradeCategory.Categories from item in submarine.GetItems(UpgradeManager.UpgradeAlsoConnectedSubs) where category.CanBeApplied(item) && !item.NonInteractable select item).Cast().ToList(); + List pointsOfInterest = (from category in UpgradeCategory.Categories from item in submarine.GetItems(UpgradeManager.UpgradeAlsoConnectedSubs) where category.CanBeApplied(item, null) && item.IsPlayerTeamInteractable select item).Cast().ToList(); List ids = GameMain.GameSession.SubmarineInfo?.LeftBehindDockingPortIDs ?? new List(); pointsOfInterest.AddRange(submarine.GetItems(UpgradeManager.UpgradeAlsoConnectedSubs).Where(item => ids.Contains(item.ID))); @@ -1112,7 +1120,7 @@ namespace Barotrauma List frames = new List(); foreach (var (item, guiFrame) in itemPreviews) { - if (category.CanBeApplied(item)) + if (category.CanBeApplied(item, null)) { frames.Add(guiFrame); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index 00b777e47..5624a6820 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -557,6 +557,7 @@ namespace Barotrauma GameModePreset.Init(); + SaveUtil.DeleteDownloadedSubs(); SubmarineInfo.RefreshSavedSubs(); TitleScreen.LoadState = 65.0f; @@ -1113,7 +1114,6 @@ namespace Barotrauma { new Pair(TextManager.Get("EditorDisclaimerWikiLink"), TextManager.Get("EditorDisclaimerWikiUrl")), new Pair(TextManager.Get("EditorDisclaimerDiscordLink"), TextManager.Get("EditorDisclaimerDiscordUrl")), - new Pair(TextManager.Get("EditorDisclaimerForumLink"), TextManager.Get("EditorDisclaimerForumUrl")), }; foreach (var link in links) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs index bd8ac4edc..fba84ab41 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CargoManager.cs @@ -39,61 +39,37 @@ namespace Barotrauma private List SoldEntities { get; } = new List(); - public List GetSellableItems(Character character) + public IEnumerable GetSellableItems(Character character) { if (character == null) { return new List(); } - // Only consider items which have been: // a) sold in singleplayer or confirmed by server (SellStatus.Confirmed); or // b) sold locally in multiplayer (SellStatus.Local), but the client has not received a campaing state update yet after selling them - var soldEntities = SoldEntities.Where(se => se.Status != SoldEntity.SellStatus.Unconfirmed); - - var sellables = Item.ItemList.FindAll(i => i?.Prefab != null && !i.Removed && - i.GetRootInventoryOwner() == character && - !i.SpawnedInOutpost && - (i.ContainedItems == null || i.ContainedItems.None() || i.ContainedItems.All(ci => soldEntities.Any(se => se.Item == ci))) && - (i.Condition >= 0.9f * i.MaxCondition || i.Prefab.AllowSellingWhenBroken) && soldEntities.None(se => se.Item == i)); - - // Prevent selling items in equipment slots + var confirmedSoldEntities = SoldEntities.Where(se => se.Status != SoldEntity.SellStatus.Unconfirmed); + // The bag slot is intentionally left out since we want to be able to sell items from there var equipmentSlots = new List() { InvSlotType.Head, InvSlotType.InnerClothes, InvSlotType.OuterClothes, InvSlotType.Headset, InvSlotType.Card }; - foreach (InvSlotType slot in equipmentSlots) + return character.Inventory.FindAllItems(item => { - var index = character.Inventory.FindLimbSlot(slot); - if (character.Inventory.Items[index] is Item item) - { - // Don't prevent selling of items which can only be put in equipment slots (like diving suits) - if (item.AllowedSlots.Contains(InvSlotType.Any)) - { - sellables.Remove(item); - } - } - } + if (item.SpawnedInOutpost) { return false; } + if (!item.Prefab.AllowSellingWhenBroken && item.ConditionPercentage < 90.0f) { return false; } + if (confirmedSoldEntities.Any(it => it.Item == item)) { return false; } + // There must be no contained items or the contained items must be confirmed as sold + if (!item.ContainedItems.All(it => confirmedSoldEntities.Any(se => se.Item == it))) { return false; } + // Item must be in a non-equipment slot if possible + if (!item.AllowedSlots.All(s => equipmentSlots.Contains(s)) && IsInEquipmentSlot(item)) { return false; } + // Item must not be contained inside an item in an equipment slot + if (item.GetRootContainer() is Item rootContainer && IsInEquipmentSlot(rootContainer)) { return false; } + return true; + }, recursive: true).Distinct(); - // Prevent selling items contained inside equipped items - foreach (InvSlotType slot in equipmentSlots) + bool IsInEquipmentSlot(Item item) { - var index = character.Inventory.FindLimbSlot(slot); - if (character.Inventory.Items[index] is Item item && - item.ContainedItems != null && item.AllowedSlots.Contains(InvSlotType.Any)) + foreach (InvSlotType slot in equipmentSlots) { - RemoveContainedFromSellables(item); + if (character.Inventory.IsInLimbSlot(item, slot)) { return true; } } + return false; } - - void RemoveContainedFromSellables(Item item) - { - foreach (Item containedItem in item.ContainedItems) - { - if (containedItem == null) { continue; } - if (containedItem.ContainedItems != null) - { - RemoveContainedFromSellables(containedItem); - } - sellables.Remove(containedItem); - } - } - - return sellables; } public void SetItemsInBuyCrate(List items) @@ -149,15 +125,20 @@ namespace Barotrauma var canAddToRemoveQueue = campaign.IsSinglePlayer && Entity.Spawner != null; var sellerId = GameMain.Client?.ID ?? 0; + // Check all the prices before starting the transaction + // to make sure the modifiers stay the same for the whole transaction + Dictionary sellValues = GetSellValuesAtCurrentLocation(itemsToSell.Select(i => i.ItemPrefab)); + foreach (PurchasedItem item in itemsToSell) { - var itemValue = GetSellValueAtCurrentLocation(item.ItemPrefab, quantity: item.Quantity); + var itemValue = item.Quantity * sellValues[item.ItemPrefab]; // check if the store can afford the item if (Location.StoreCurrentBalance < itemValue) { continue; } - var matchingItems = itemsInInventory.FindAll(i => i.Prefab == item.ItemPrefab); - if (matchingItems.Count <= item.Quantity) + // TODO: Write logic for prioritizing certain items over others (e.g. lone Battery Cell should be preferred over one inside a Stun Baton) + var matchingItems = itemsInInventory.Where(i => i.Prefab == item.ItemPrefab); + if (matchingItems.Count() <= item.Quantity) { foreach (Item i in matchingItems) { @@ -170,7 +151,7 @@ namespace Barotrauma { for (int i = 0; i < item.Quantity; i++) { - var matchingItem = matchingItems[i]; + var matchingItem = matchingItems.ElementAt(i); SoldItems.Add(new SoldItem(matchingItem.Prefab, matchingItem.ID, canAddToRemoveQueue, sellerId)); SoldEntities.Add(campaign.IsSinglePlayer ? SoldEntity.CreateInSinglePlayer(matchingItem) : SoldEntity.CreateInMultiPlayer(matchingItem)); if (canAddToRemoveQueue) { Entity.Spawner.AddToRemoveQueue(matchingItem); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index b905ecbb2..dc1e16330 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -134,6 +134,7 @@ namespace Barotrauma isScrollBarOnDefaultSide: false) { AutoHideScrollBar = false, + CanBeFocused = false, OnSelected = (component, userData) => false, SelectMultiple = false, Spacing = (int)(GUI.Scale * 10) @@ -232,7 +233,7 @@ namespace Barotrauma }; } - var reports = Order.PrefabList.FindAll(o => o.IsReport && o.SymbolSprite != null); + var reports = Order.PrefabList.FindAll(o => o.IsReport && o.SymbolSprite != null && !o.Hidden); if (reports.None()) { DebugConsole.ThrowError("No valid orders for report buttons found! Cannot create report buttons. The orders for the report buttons must have 'targetallcharacters' attribute enabled and a valid 'symbolsprite' defined."); @@ -252,7 +253,7 @@ namespace Barotrauma //report buttons foreach (Order order in reports) { - if (!order.IsReport || order.SymbolSprite == null) { continue; } + if (!order.IsReport || order.SymbolSprite == null || order.Hidden) { continue; } var btn = new GUIButton(new RectTransform(new Point(ReportButtonFrame.Rect.Width), ReportButtonFrame.RectTransform), style: null) { OnClicked = (GUIButton button, object userData) => @@ -602,11 +603,11 @@ namespace Barotrauma private WifiComponent GetHeadset(Character character, bool requireEquipped) { - if (character?.Inventory == null) return null; + if (character?.Inventory == null) { return null; } - var radioItem = character.Inventory.Items.FirstOrDefault(it => it != null && it.GetComponent() != null); - if (radioItem == null) return null; - if (requireEquipped && !character.HasEquippedItem(radioItem)) return null; + var radioItem = character.Inventory.AllItems.FirstOrDefault(it => it.GetComponent() != null); + if (radioItem == null) { return null; } + if (requireEquipped && !character.HasEquippedItem(radioItem)) { return null; } return radioItem.GetComponent(); } @@ -687,18 +688,10 @@ namespace Barotrauma else if(order.IsIgnoreOrder) { WallSection ws = null; - if (order.TargetType == Order.OrderTargetType.Entity && order.TargetEntity is MapEntity me) + if (order.TargetType == Order.OrderTargetType.Entity && order.TargetEntity is IIgnorable ignorable) { - if (order.Identifier == "ignorethis") - { - me.SetIgnoreByAI(true); - AddOrder(new Order(order.Prefab ?? order, order.TargetEntity, order.TargetItemComponent, orderGiver), null); - } - else - { - me.SetIgnoreByAI(false); - ActiveOrders.RemoveAll(p => p.First.Identifier == "ignorethis" && p.First.TargetEntity == order.TargetEntity); - } + ignorable.OrderedToBeIgnored = order.Identifier == "ignorethis"; + AddOrder(new Order(order.Prefab ?? order, order.TargetEntity, order.TargetItemComponent, orderGiver), null); } else if (order.TargetType == Order.OrderTargetType.WallSection && order.TargetEntity is Structure s) { @@ -706,18 +699,14 @@ namespace Barotrauma ws = s.GetSection(wallSectionIndex); if (ws != null) { - if (order.Identifier == "ignorethis") - { - ws.SetIgnoreByAI(true); - AddOrder(new Order(order.Prefab ?? order, s, wallSectionIndex, orderGiver), null); - } - else - { - ws.SetIgnoreByAI(false); - ActiveOrders.RemoveAll(p => p.First.Identifier == "ignorethis" && p.First.TargetEntity == s && p.First.WallSectionIndex == wallSectionIndex); - } + ws.OrderedToBeIgnored = order.Identifier == "ignorethis"; + AddOrder(new Order(order.Prefab ?? order, s, wallSectionIndex, orderGiver), null); } } + else + { + return; + } if (ws != null) { @@ -781,7 +770,16 @@ namespace Barotrauma { currentOrderInfo = (OrderInfo)currentOrderIcon.UserData; // No need to recreate icons if the current order matches the new order - if (currentOrderInfo.Value.MatchesOrder(order, option)) { return; } + if (currentOrderInfo.Value.MatchesOrder(order, option)) + { + currentOrderIcon.UserData = new OrderInfo(order, option); + if (currentOrderIcon.FindChild(c => (string)c.UserData == "colorsource") is GUIImage image) + { + image.Sprite = GetOrderIconSprite(order, option); + image.ToolTip = CreateOrderTooltip(order, option); + } + return; + } } // Remove the current order icon @@ -826,7 +824,10 @@ namespace Barotrauma } }; - CreateNodeIcon(orderFrame.RectTransform, order.SymbolSprite, order.Color, tooltip: order.Name); + CreateNodeIcon(orderFrame.RectTransform, + GetOrderIconSprite(order, option), + order.Color, + tooltip: CreateOrderTooltip(order, option)); new GUIImage(new RectTransform(Vector2.One, orderFrame.RectTransform), cancelIcon, scaleToFit: true) { @@ -870,11 +871,10 @@ namespace Barotrauma new RectTransform(new Vector2(0.8f), prevOrderFrame.RectTransform, anchor: Anchor.BottomLeft), style: null); - CreateNodeIcon( - prevOrderIconFrame.RectTransform, - previousOrderInfo.Order.SymbolSprite, + CreateNodeIcon(prevOrderIconFrame.RectTransform, + GetOrderIconSprite(previousOrderInfo), previousOrderInfo.Order.Color, - tooltip: previousOrderInfo.Order.Name); + tooltip: CreateOrderTooltip(previousOrderInfo)); foreach (GUIComponent c in prevOrderIconFrame.Children) { @@ -947,6 +947,48 @@ namespace Barotrauma private IEnumerable GetPreviousOrderIcons(GUILayoutGroup characterComponent) => characterComponent?.FindChildren(c => c?.UserData is OrderInfo orderInfo && orderInfo.ComponentIdentifier == "previousorder"); + private string CreateOrderTooltip(Order order, string option) + { + if (order == null) { return ""; } + if (!string.IsNullOrEmpty(option)) + { + return TextManager.GetWithVariables("crewlistordericontooltip", + new string[2] { "[ordername]", "[orderoption]" }, + new string[2] { order.Name, order.GetOptionName(option) }); + } + else if (order.TargetEntity is Item targetItem && order.MinimapIcons.ContainsKey(targetItem.Prefab.Identifier)) + { + return TextManager.GetWithVariables("crewlistordericontooltip", + new string[2] { "[ordername]", "[orderoption]" }, + new string[2] { order.Name, targetItem.Name }); + } + else + { + return order.Name; + } + } + + private string CreateOrderTooltip(OrderInfo orderInfo) => + CreateOrderTooltip(orderInfo.Order, orderInfo.OrderOption); + + private Sprite GetOrderIconSprite(Order order, string option) + { + if (order == null) { return null; } + Sprite sprite = null; + if (option != null && order.Prefab.OptionSprites.Any()) + { + order.Prefab.OptionSprites.TryGetValue(option, out sprite); + } + if (sprite == null && order.TargetEntity is Item targetItem && order.MinimapIcons.Any()) + { + order.MinimapIcons.TryGetValue(targetItem.Prefab.Identifier, out sprite); + } + return sprite ?? order.SymbolSprite; + } + + private Sprite GetOrderIconSprite(OrderInfo orderInfo) => + GetOrderIconSprite(orderInfo.Order, orderInfo.OrderOption); + #endregion #region Updating and drawing the UI @@ -982,7 +1024,7 @@ namespace Barotrauma public void CreateModerationContextMenu(Point mousePos, Client client) { - if (IsSinglePlayer || client == null || (GameMain.NetworkMember?.ConnectedClients?.All(match => match != client) ?? true)) { return; } + if (IsSinglePlayer || client == null || (!GameMain.Client?.PreviouslyConnectedClients?.Contains(client) ?? true)) { return; } contextMenu = new GUIFrame(new RectTransform(new Vector2(0.1f, 0.15f), GUI.Canvas) { ScreenSpaceOffset = mousePos }, style: "GUIToolTip") { UserData = client }; @@ -1032,19 +1074,22 @@ namespace Barotrauma UserData = "promote" }; - new GUITextBlock(new RectTransform(Point.Zero, parent), TextManager.Get(client.MutedLocally ? "unmute" : "mute"), font: GUI.SmallFont) + if (GameMain.Client.ConnectedClients.Contains(client)) { - Padding = new Vector4(4), - Enabled = client.ID != GameMain.Client?.ID, - UserData = "mute" - }; + new GUITextBlock(new RectTransform(Point.Zero, parent), TextManager.Get(client.MutedLocally ? "unmute" : "mute"), font: GUI.SmallFont) + { + Padding = new Vector4(4), + Enabled = client.ID != GameMain.Client?.ID, + UserData = "mute" + }; - new GUITextBlock(new RectTransform(Point.Zero, parent), TextManager.Get(canKick ? "kick" : "votetokick"), font: GUI.SmallFont) - { - Padding = new Vector4(4), - Enabled = client.ID != GameMain.Client?.ID && client.AllowKicking, - UserData = canKick ? "kick" : "votekick" - }; + new GUITextBlock(new RectTransform(Point.Zero, parent), TextManager.Get(canKick ? "kick" : "votetokick"), font: GUI.SmallFont) + { + Padding = new Vector4(4), + Enabled = client.ID != GameMain.Client?.ID && client.AllowKicking, + UserData = canKick ? "kick" : "votekick" + }; + } new GUITextBlock(new RectTransform(Point.Zero, parent), TextManager.Get("ban"), font: GUI.SmallFont) { @@ -1363,23 +1408,13 @@ namespace Barotrauma isSelectionHighlighted = false; } - if (!CanIssueOrders) - { - DisableCommandUI(); - } - else if (PlayerInput.SecondaryMouseButtonClicked() && characterContext == null && - (optionNodes.Any(n => GUI.IsMouseOn(n.Item1)) || shortcutNodes.Any(n => GUI.IsMouseOn(n)))) - { - var node = optionNodes.Find(n => GUI.IsMouseOn(n.Item1))?.Item1 ?? shortcutNodes.Find(n => GUI.IsMouseOn(n)); - // Make sure the node is for an option-less order or an order option - if ((node.UserData is Order order && !order.HasOptions && (!order.MustSetTarget || itemContext != null)) || node.UserData is Tuple) - { - CreateAssignmentNodes(node); - } - } + // When using Deselect to close the interface, make sure it's not a seconday mouse button click on a node + // That should be reserved for opening manual assignment + var hitDeselect = PlayerInput.KeyHit(InputType.Deselect) && (!PlayerInput.SecondaryMouseButtonClicked() || + (optionNodes.None(n => GUI.IsMouseOn(n.Item1)) && shortcutNodes.None(n => GUI.IsMouseOn(n)))); // TODO: Consider using HUD.CloseHUD() instead of KeyHit(Escape), the former method is also used for health UI - else if ((PlayerInput.KeyHit(InputType.Command) && selectedNode == null && !clicklessSelectionActive) || - PlayerInput.KeyHit(InputType.Deselect) || PlayerInput.KeyHit(Keys.Escape)) + if (hitDeselect || PlayerInput.KeyHit(Keys.Escape) || !CanIssueOrders || + (PlayerInput.KeyHit(InputType.Command) && selectedNode == null && !clicklessSelectionActive)) { DisableCommandUI(); } @@ -1438,7 +1473,14 @@ namespace Barotrauma timeSelected += deltaTime; if (timeSelected >= selectionTime) { - selectedNode.OnClicked?.Invoke(selectedNode, selectedNode.UserData); + if (PlayerInput.IsShiftDown() && selectedNode.OnSecondaryClicked != null) + { + selectedNode.OnSecondaryClicked.Invoke(selectedNode, selectedNode.UserData); + } + else + { + selectedNode.OnClicked?.Invoke(selectedNode, selectedNode.UserData); + } ResetNodeSelection(); } else if (timeSelected >= 0.15f && !isSelectionHighlighted) @@ -1463,7 +1505,15 @@ namespace Barotrauma { if (node.Item2 != Keys.None && PlayerInput.KeyHit(node.Item2)) { - (node.Item1 as GUIButton)?.OnClicked?.Invoke(node.Item1 as GUIButton, node.Item1.UserData); + var b = node.Item1 as GUIButton; + if (PlayerInput.IsShiftDown() && b?.OnSecondaryClicked != null) + { + b.OnSecondaryClicked.Invoke(node.Item1 as GUIButton, node.Item1.UserData); + } + else + { + b?.OnClicked?.Invoke(node.Item1 as GUIButton, node.Item1.UserData); + } ResetNodeSelection(); hotkeyHit = true; break; @@ -1947,7 +1997,12 @@ namespace Barotrauma } // When the mini map is shown, always position the return node on the bottom - var offset = node?.UserData is Order order && order.GetMatchingItems(true).Count > 1 ? + List matchingItems = null; + if (node?.UserData is Order order) + { + matchingItems = order.GetMatchingItems(true, interactableFor: characterContext ?? Character.Controlled); + } + var offset = matchingItems != null && matchingItems.Count > 1 ? new Point(0, (int)(returnNodeDistanceModifier * nodeDistance)) : node.RectTransform.AbsoluteOffset.Multiply(-returnNodeDistanceModifier); SetReturnNode(centerNode, offset); @@ -2027,6 +2082,7 @@ namespace Barotrauma SetCharacterTooltip(c, characterContext); } node.OnClicked = null; + node.OnSecondaryClicked = null; centerNode = node; } @@ -2042,6 +2098,7 @@ namespace Barotrauma c.ToolTip = TextManager.Get("commandui.return"); } node.OnClicked = NavigateBackward; + node.OnSecondaryClicked = null; returnNode = node; } @@ -2072,11 +2129,14 @@ namespace Barotrauma private void RemoveOptionNodes() { - optionNodes.ForEach(node => commandFrame.RemoveChild(node.Item1)); + if (commandFrame != null) + { + optionNodes.ForEach(node => commandFrame.RemoveChild(node.Item1)); + shortcutNodes.ForEach(node => commandFrame.RemoveChild(node)); + commandFrame.RemoveChild(expandNode); + } optionNodes.Clear(); - shortcutNodes.ForEach(node => commandFrame.RemoveChild(node)); shortcutNodes.Clear(); - commandFrame.RemoveChild(expandNode); expandNode = null; expandNodeHotkey = Keys.None; RemoveExtraOptionNodes(); @@ -2084,7 +2144,10 @@ namespace Barotrauma private void RemoveExtraOptionNodes() { - extraOptionNodes.ForEach(node => commandFrame.RemoveChild(node)); + if (commandFrame != null) + { + extraOptionNodes.ForEach(node => commandFrame.RemoveChild(node)); + } extraOptionNodes.Clear(); } @@ -2125,7 +2188,8 @@ namespace Barotrauma shortcutNodes.Clear(); - if (shortcutNodes.Count < maxShortCutNodeCount && sub.GetItems(false).Find(i => i.HasTag("reactor") && !i.NonInteractable)?.GetComponent() is Reactor reactor) + if (shortcutNodes.Count < maxShortCutNodeCount && + sub.GetItems(false).Find(i => i.HasTag("reactor") && i.IsPlayerTeamInteractable)?.GetComponent() is Reactor reactor) { var reactorOutput = -reactor.CurrPowerConsumption; // If player is not an engineer AND the reactor is not powered up AND nobody is using the reactor @@ -2144,7 +2208,7 @@ namespace Barotrauma // If player is not a captain AND nobody is using the nav terminal AND the nav terminal is powered up // --> Create shortcut node for Steer order if (shortcutNodes.Count < maxShortCutNodeCount && (Character.Controlled == null || Character.Controlled.Info?.Job?.Prefab != JobPrefab.Get("captain")) && - sub.GetItems(false).Find(i => i.HasTag("navterminal") && !i.NonInteractable) is Item nav && characters.None(c => c.SelectedConstruction == nav) && + sub.GetItems(false).Find(i => i.HasTag("navterminal") && i.IsPlayerTeamInteractable) is Item nav && characters.None(c => c.SelectedConstruction == nav) && nav.GetComponent() is Steering steering && steering.Voltage > steering.MinVoltage) { shortcutNodes.Add( @@ -2195,7 +2259,7 @@ namespace Barotrauma (n.UserData is Tuple orderWithOption && orderWithOption.Item1.Identifier == orderIdentifier)) && !orderPrefab.IsReport && orderPrefab.Category != null) { - if (!orderPrefab.MustSetTarget || orderPrefab.GetMatchingItems(sub, true).Any()) + if (!orderPrefab.MustSetTarget || orderPrefab.GetMatchingItems(sub, true, interactableFor: characterContext ?? Character.Controlled).Any()) { shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, orderPrefab, -1)); } @@ -2245,7 +2309,8 @@ namespace Barotrauma { order = orders[i]; disableNode = !CanSomeoneHearCharacter() || - (order.MustSetTarget && (order.ItemComponentType != null || order.TargetItems.Length > 0) && order.GetMatchingItems(true).None()); + (order.MustSetTarget && (order.ItemComponentType != null || order.TargetItems.Length > 0) && + order.GetMatchingItems(true, interactableFor: characterContext ?? Character.Controlled).None()); optionNodes.Add(new Tuple( CreateOrderNode(nodeSize, commandFrame.RectTransform, offsets[i].ToPoint(), order, (i + 1) % 10, disableNode: disableNode, checkIfOrderCanBeHeard: false), !disableNode ? Keys.D0 + (i + 1) % 10 : Keys.None)); @@ -2262,7 +2327,7 @@ namespace Barotrauma string orderIdentifier; // Check if targeting an item or a hull - if (itemContext != null && !itemContext.NonInteractable) + if (itemContext != null && itemContext.IsPlayerTeamInteractable) { ItemComponent targetComponent; foreach (Order p in Order.PrefabList) @@ -2314,9 +2379,12 @@ namespace Barotrauma if (contextualOrders.None()) { orderIdentifier = "cleanupitems"; - if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier)) && AIObjectiveCleanupItems.IsValidTarget(itemContext, Character.Controlled, checkInventory: false)) + if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier))) { - contextualOrders.Add(new Order(Order.GetPrefab(orderIdentifier), itemContext, targetItem: null, Character.Controlled)); + if (AIObjectiveCleanupItems.IsValidTarget(itemContext, Character.Controlled, checkInventory: false) || AIObjectiveCleanupItems.IsValidContainer(itemContext, Character.Controlled)) + { + contextualOrders.Add(new Order(Order.GetPrefab(orderIdentifier), itemContext, targetItem: null, Character.Controlled)); + } } } @@ -2332,6 +2400,35 @@ namespace Barotrauma } } + void AddIgnoreOrder(IIgnorable target) + { + var orderIdentifier = "ignorethis"; + if (!target.OrderedToBeIgnored && contextualOrders.None(o => o.Identifier == orderIdentifier)) + { + AddOrder(); + } + else + { + orderIdentifier = "unignorethis"; + if (target.OrderedToBeIgnored && contextualOrders.None(o => o.Identifier == orderIdentifier)) + { + AddOrder(); + } + } + + void AddOrder() + { + if (target is WallSection ws) + { + contextualOrders.Add(new Order(Order.GetPrefab(orderIdentifier), ws.Wall, ws.Wall.Sections.IndexOf(ws), orderGiver: Character.Controlled)); + } + else + { + contextualOrders.Add(new Order(Order.GetPrefab(orderIdentifier), target as Entity, null, Character.Controlled)); + } + } + } + orderIdentifier = "wait"; if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier))) { @@ -2366,35 +2463,6 @@ namespace Barotrauma CreateOrderNode(nodeSize, commandFrame.RectTransform, offsets[i].ToPoint(), contextualOrders[i], (i + 1) % 10, disableNode: disableNode, checkIfOrderCanBeHeard: false), !disableNode ? Keys.D0 + (i + 1) % 10 : Keys.None)); } - - void AddIgnoreOrder(ISpatialEntity target) - { - var orderIdentifier = "ignorethis"; - if (!target.IgnoreByAI && contextualOrders.None(o => o.Identifier.Equals(orderIdentifier))) - { - AddOrder(orderIdentifier, target); - } - else - { - orderIdentifier = "unignorethis"; - if (target.IgnoreByAI && contextualOrders.None(o => o.Identifier.Equals(orderIdentifier))) - { - AddOrder(orderIdentifier, target); - } - } - - void AddOrder(string id, ISpatialEntity target) - { - if (target is WallSection ws) - { - contextualOrders.Add(new Order(Order.GetPrefab(orderIdentifier), ws.Wall, ws.Wall.Sections.IndexOf(ws), orderGiver: Character.Controlled)); - } - else - { - contextualOrders.Add(new Order(Order.GetPrefab(orderIdentifier), target as Entity, null, Character.Controlled)); - } - } - } } // TODO: there's duplicate logic here and above -> would be better to refactor so that the conditions are only defined in one place @@ -2404,6 +2472,7 @@ namespace Barotrauma if (Order.PrefabList.Any(o => item.HasTag(o.TargetItems))) { return true; } if (Order.PrefabList.Any(o => o.TryGetTargetItemComponent(item, out _))) { return true; } if (AIObjectiveCleanupItems.IsValidTarget(item, Character.Controlled, checkInventory: false)) { return true; } + if (AIObjectiveCleanupItems.IsValidContainer(item, Character.Controlled)) { return true; } if (item.Repairables.Any(r => item.ConditionPercentage < r.RepairThreshold)) { return true; } var operateWeaponsPrefab = Order.GetPrefab("operateweapons"); @@ -2435,7 +2504,7 @@ namespace Barotrauma // so we know to directly target that with the order if (!mustSetOptionOrTarget && order.MustSetTarget && itemContext == null) { - var matchingItems = order.GetMatchingItems(GetTargetSubmarine(), true); + var matchingItems = order.GetMatchingItems(GetTargetSubmarine(), true, interactableFor: characterContext ?? Character.Controlled); if (matchingItems.Count > 1) { mustSetOptionOrTarget = true; @@ -2470,9 +2539,14 @@ namespace Barotrauma } return true; }; - // TODO: Might need to edit the tooltip + if (CanOpenManualAssignment(node)) + { + node.OnSecondaryClicked = (button, _) => CreateAssignmentNodes(button); + } + var showAssignmentTooltip = !mustSetOptionOrTarget && characterContext == null && !order.MustManuallyAssign && !order.TargetAllCharacters; + var orderName = GetOrderNameBasedOnContextuality(order); var icon = CreateNodeIcon(node.RectTransform, order.SymbolSprite, order.Color, - tooltip: mustSetOptionOrTarget || characterContext != null ? order.Name : order.Name + + tooltip: !showAssignmentTooltip ? orderName : orderName + "\n" + (!PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.leftmouse") : TextManager.Get("input.rightmouse")) + ": " + TextManager.Get("commandui.quickassigntooltip") + "\n" + (!PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.rightmouse") : TextManager.Get("input.leftmouse")) + ": " + TextManager.Get("commandui.manualassigntooltip")); @@ -2491,7 +2565,7 @@ namespace Barotrauma private void CreateOrderOptions(Order order) { Submarine submarine = GetTargetSubmarine(); - var matchingItems = (itemContext == null && order.MustSetTarget) ? order.GetMatchingItems(submarine, true) : new List(); + var matchingItems = (itemContext == null && order.MustSetTarget) ? order.GetMatchingItems(submarine, true, interactableFor: characterContext ?? Character.Controlled) : new List(); //more than one target item -> create a minimap-like selection with a pic of the sub if (itemContext == null && matchingItems.Count > 1) @@ -2572,30 +2646,33 @@ namespace Barotrauma Stretch = true }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), optionContainer.RectTransform), item != null ? item.Name : order.Name); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), optionContainer.RectTransform), + item?.Name ?? GetOrderNameBasedOnContextuality(order)); for (int i = 0; i < order.Options.Length; i++) { - optionNodes.Add(new Tuple( - new GUIButton( - new RectTransform(new Vector2(1.0f, 0.2f), optionContainer.RectTransform), - text: order.GetOptionName(i), - style: "GUITextBox") + var optionButton = new GUIButton( + new RectTransform(new Vector2(1.0f, 0.2f), optionContainer.RectTransform), + text: order.GetOptionName(i), style: "GUITextBox") + { + UserData = new Tuple( + item == null ? order : new Order(order, item, order.GetTargetItemComponent(item)), + order.Options[i]), + Font = GUI.SmallFont, + OnClicked = (_, userData) => { - UserData = new Tuple( - item == null ? order : new Order(order, item, order.GetTargetItemComponent(item)), - order.Options[i]), - Font = GUI.SmallFont, - OnClicked = (_, userData) => - { - if (!CanIssueOrders) { return false; } - var o = userData as Tuple; - SetCharacterOrder(characterContext ?? GetCharacterForQuickAssignment(o.Item1), o.Item1, o.Item2, Character.Controlled); - DisableCommandUI(); - return true; - } - }, - Keys.None)); + if (!CanIssueOrders) { return false; } + var o = userData as Tuple; + SetCharacterOrder(characterContext ?? GetCharacterForQuickAssignment(o.Item1), o.Item1, o.Item2, Character.Controlled); + DisableCommandUI(); + return true; + } + }; + if (CanOpenManualAssignment(optionButton)) + { + optionButton.OnSecondaryClicked = (button, _) => CreateAssignmentNodes(button); + } + optionNodes.Add(new Tuple(optionButton, Keys.None)); } } else @@ -2610,7 +2687,7 @@ namespace Barotrauma { UserData = userData, Font = GUI.SmallFont, - ToolTip = item?.Name ?? order.Name, + ToolTip = item?.Name ?? GetOrderNameBasedOnContextuality(order), OnClicked = (_, userData) => { if (!CanIssueOrders) { return false; } @@ -2620,7 +2697,10 @@ namespace Barotrauma return true; } }; - + if (CanOpenManualAssignment(optionElement)) + { + optionElement.OnSecondaryClicked = (button, _) => CreateAssignmentNodes(button); + } Sprite icon = null; order.MinimapIcons?.TryGetValue(item.Prefab.Identifier, out icon); if (item.Prefab.MinimapIcon != null) @@ -2677,11 +2757,16 @@ namespace Barotrauma return true; } }; + if (CanOpenManualAssignment(node)) + { + node.OnSecondaryClicked = (button, _) => CreateAssignmentNodes(button); + } node.RectTransform.MoveOverTime(offset, CommandNodeAnimDuration); GUIImage icon = null; if (order.Prefab.OptionSprites.TryGetValue(option, out Sprite sprite)) { + var showAssignmentTooltip = characterContext == null && !order.MustManuallyAssign && !order.TargetAllCharacters; icon = CreateNodeIcon(node.RectTransform, sprite, order.Color, tooltip: characterContext != null ? optionName : optionName + "\n" + (!PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.leftmouse") : TextManager.Get("input.rightmouse")) + ": " + TextManager.Get("commandui.quickassigntooltip") + @@ -2700,13 +2785,13 @@ namespace Barotrauma return node; } - private void CreateAssignmentNodes(GUIComponent node) + private bool CreateAssignmentNodes(GUIComponent node) { var order = (node.UserData is Order) ? new Tuple(node.UserData as Order, null) : node.UserData as Tuple; var characters = GetCharactersForManualAssignment(order.Item1); - if (characters.None()) { return; } + if (characters.None()) { return false; } if (!(optionNodes.Find(n => n.Item1 == node) is Tuple optionNode) || !optionNodes.Remove(optionNode)) { @@ -2791,7 +2876,7 @@ namespace Barotrauma CreateHotkeyIcon(returnNode.RectTransform, hotkey % 10, true); returnNodeHotkey = Keys.D0 + hotkey % 10; expandNodeHotkey = Keys.None; - return; + return true; } extraOptionCharacters.Clear(); @@ -2816,6 +2901,7 @@ namespace Barotrauma expandNodeHotkey = Keys.D0 + hotkey % 10; CreateHotkeyIcon(returnNode.RectTransform, ++hotkey % 10, true); returnNodeHotkey = Keys.D0 + hotkey % 10; + return true; } private Vector2[] GetAssignmentNodeOffsets(int characters, bool firstRing = true) @@ -3083,7 +3169,7 @@ namespace Barotrauma if (Character.Controlled != null) { // Pick the second main sub when we have two teams (in combat mission) - if (Character.Controlled.TeamID == Character.TeamType.Team2 && Submarine.MainSubs.Length > 1) + if (Character.Controlled.TeamID == CharacterTeamType.Team2 && Submarine.MainSubs.Length > 1) { sub = Submarine.MainSubs[1]; } @@ -3105,7 +3191,29 @@ namespace Barotrauma component.ToolTip = tooltip; } + private string GetOrderNameBasedOnContextuality(Order order) + { + if (order == null) { return ""; } + if (isContextual) { return order.ContextualName; } + return order.Name; + } + #region Crew Member Assignment Logic + private bool CanOpenManualAssignment(GUIComponent node) + { + if (node == null || characterContext != null) { return false; } + if (node.UserData is Tuple orderInfo) + { + return !orderInfo.Item1.TargetAllCharacters; + } + if (node.UserData is Order order) + { + return !order.TargetAllCharacters && !order.HasOptions && + (!order.MustSetTarget || itemContext != null || + order.GetMatchingItems(GetTargetSubmarine(), true, interactableFor: Character.Controlled).Count < 2); + } + return false; + } private Character GetCharacterForQuickAssignment(Order order) { @@ -3145,7 +3253,7 @@ namespace Barotrauma // 4. Prioritize bots over player controlled characters .ThenByDescending(c => c.IsBot) // 5. Use the priority value of the current objective - .ThenBy(c => c.AIController?.ObjectiveManager.CurrentObjective?.Priority) + .ThenBy(c => c.AIController is HumanAIController humanAI ? humanAI.ObjectiveManager.CurrentObjective?.Priority : 0) // 6. Prioritize those with the best skill for the order .ThenByDescending(c => c.GetSkillLevel(order.AppropriateSkill)); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index 8abd2ea9f..6a189aa73 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -450,7 +450,7 @@ namespace Barotrauma { if (mb is GUIMessageBox msgBox) { - if (mb.UserData is Pair pair && pair.First.Equals("conversationaction", StringComparison.OrdinalIgnoreCase)) + if (ReadyCheck.IsReadyCheck(mb) || mb.UserData is Pair pair && pair.First.Equals("conversationaction", StringComparison.OrdinalIgnoreCase)) { msgBox.Close(); } @@ -812,8 +812,7 @@ namespace Barotrauma return; } Load(doc.Root.Element("MultiPlayerCampaign")); - SubmarineInfo selectedSub; - GameMain.GameSession.OwnedSubmarines = SaveUtil.LoadOwnedSubmarines(doc, out selectedSub); + GameMain.GameSession.OwnedSubmarines = SaveUtil.LoadOwnedSubmarines(doc, out SubmarineInfo selectedSub); GameMain.GameSession.SubmarineInfo = selectedSub; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index 33cc48da0..71364cf6b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -11,6 +11,8 @@ namespace Barotrauma { class SinglePlayerCampaign : CampaignMode { + public const int MinimumInitialMoney = 0; + public override bool Paused { get { return ForceMapUI || CoroutineManager.IsCoroutineRunning("LevelTransition") || ShowCampaignUI && CampaignUI.SelectedTab == InteractionType.Map; } @@ -105,7 +107,6 @@ namespace Barotrauma } CampaignMetadata ??= new CampaignMetadata(this); - UpgradeManager ??= new UpgradeManager(this); InitCampaignData(); @@ -113,6 +114,9 @@ namespace Barotrauma InitUI(); Money = element.GetAttributeInt("money", 0); + PurchasedLostShuttles = element.GetAttributeBool("purchasedlostshuttles", false); + PurchasedHullRepairs = element.GetAttributeBool("purchasedhullrepairs", false); + PurchasedItemRepairs = element.GetAttributeBool("purchaseditemrepairs", false); CheatsEnabled = element.GetAttributeBool("cheatsenabled", false); if (CheatsEnabled) { @@ -137,7 +141,7 @@ namespace Barotrauma /// /// Start a completely new single player campaign /// - public static SinglePlayerCampaign StartNew(string mapSeed) + public static SinglePlayerCampaign StartNew(string mapSeed, SubmarineInfo selectedSub) { var campaign = new SinglePlayerCampaign(mapSeed); return campaign; @@ -699,6 +703,9 @@ namespace Barotrauma { XElement modeElement = new XElement("SinglePlayerCampaign", new XAttribute("money", Money), + new XAttribute("purchasedlostshuttles", PurchasedLostShuttles), + new XAttribute("purchasedhullrepairs", PurchasedHullRepairs), + new XAttribute("purchaseditemrepairs", PurchasedItemRepairs), new XAttribute("cheatsenabled", CheatsEnabled)); //save and remove all items that are in someone's inventory so they don't get included in the sub file as well diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/BasicTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/BasicTutorial.cs index 4451526c6..a98a699a6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/BasicTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/BasicTutorial.cs @@ -182,7 +182,7 @@ namespace Barotrauma.Tutorials + " Equip a screwdriver by pulling it to either of the slots with a hand symbol, and then use it on the terminal by left clicking."); while (Controlled.SelectedConstruction != steering.Item || - Controlled.SelectedItems.FirstOrDefault(i => i != null && i.Prefab.Identifier == "screwdriver") == null) + Controlled.HeldItems.FirstOrDefault(i => i.Prefab.Identifier == "screwdriver") == null) { yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; } @@ -203,16 +203,16 @@ namespace Barotrauma.Tutorials while ((Controlled.SelectedConstruction != junctionBox.Item && Controlled.SelectedConstruction != steering.Item) || - Controlled.SelectedItems.FirstOrDefault(i => i != null && i.Prefab.Identifier == "screwdriver") == null) + !Controlled.HeldItems.Any(i => i.Prefab.Identifier == "screwdriver")) { yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; } - if (Controlled.SelectedItems.FirstOrDefault(i => i != null && i.GetComponent() != null) == null) + if (!Controlled.HeldItems.Any(i => i.GetComponent() != null)) { infoBox = CreateInfoFrame("", "Equip the wire by dragging it to one of the slots with a hand symbol."); - while (Controlled.SelectedItems.FirstOrDefault(i => i != null && i.GetComponent() != null) == null) + while (!Controlled.HeldItems.Any(i => i.GetComponent() != null)) { yield return Controlled.IsDead ? CoroutineStatus.Success : CoroutineStatus.Running; } @@ -501,7 +501,7 @@ namespace Barotrauma.Tutorials do { - var weldingTool = Controlled.Inventory.Items.FirstOrDefault(i => i != null && i.Prefab.Identifier == "weldingtool"); + var weldingTool = Controlled.Inventory.FindItemByIdentifier("weldingtool"); if (weldingTool != null && weldingTool.ContainedItems.FirstOrDefault(contained => contained != null && contained.Prefab.Identifier == "weldingfueltank") != null) break; @@ -661,7 +661,10 @@ namespace Barotrauma.Tutorials //TODO: reimplement //enemy.Health = 50.0f; - enemy.AIController.State = AIState.Idle; + if (enemy.AIController is EnemyAIController enemyAI) + { + enemyAI.State = AIState.Idle; + } Vector2 targetPos = Character.Controlled.WorldPosition + new Vector2(0.0f, 3000.0f); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs index 98cc31663..f757bce5c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs @@ -101,7 +101,7 @@ namespace Barotrauma.Tutorials tutorial_submarineDoorLight = Item.ItemList.Find(i => i.HasTag("tutorial_submarinedoorlight")).GetComponent(); var medicInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, "", JobPrefab.Get("medicaldoctor")); captain_medic = Character.Create(medicInfo, captain_medicSpawnPos, "medicaldoctor"); - captain_medic.TeamID = Character.TeamType.Team1; + captain_medic.TeamID = CharacterTeamType.Team1; captain_medic.GiveJobItems(null); captain_medic.CanSpeak = captain_medic.AIController.Enabled = false; SetDoorAccess(tutorial_submarineDoor, tutorial_submarineDoorLight, false); @@ -124,17 +124,17 @@ namespace Barotrauma.Tutorials var mechanicInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, "", JobPrefab.Get("mechanic")); captain_mechanic = Character.Create(mechanicInfo, WayPoint.GetRandom(SpawnType.Human, mechanicInfo.Job, Submarine.MainSub).WorldPosition, "mechanic"); - captain_mechanic.TeamID = Character.TeamType.Team1; + captain_mechanic.TeamID = CharacterTeamType.Team1; captain_mechanic.GiveJobItems(); var securityInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, "", JobPrefab.Get("securityofficer")); captain_security = Character.Create(securityInfo, WayPoint.GetRandom(SpawnType.Human, securityInfo.Job, Submarine.MainSub).WorldPosition, "securityofficer"); - captain_security.TeamID = Character.TeamType.Team1; + captain_security.TeamID = CharacterTeamType.Team1; captain_security.GiveJobItems(); var engineerInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, "", JobPrefab.Get("engineer")); captain_engineer = Character.Create(engineerInfo, WayPoint.GetRandom(SpawnType.Human, engineerInfo.Job, Submarine.MainSub).WorldPosition, "engineer"); - captain_engineer.TeamID = Character.TeamType.Team1; + captain_engineer.TeamID = CharacterTeamType.Team1; captain_engineer.GiveJobItems(); captain_mechanic.CanSpeak = captain_security.CanSpeak = captain_engineer.CanSpeak = false; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs index 01951839c..a64f5e678 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs @@ -80,34 +80,34 @@ namespace Barotrauma.Tutorials var assistantInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, "", JobPrefab.Get("assistant")); patient1 = Character.Create(assistantInfo, patientHull1.WorldPosition, "1"); - patient1.TeamID = Character.TeamType.Team1; + patient1.TeamID = CharacterTeamType.Team1; patient1.GiveJobItems(null); patient1.CanSpeak = false; - patient1.AddDamage(patient1.WorldPosition, new List() { new Affliction(AfflictionPrefab.Burn, 45.0f) }, stun: 0, playSound: false); + patient1.AddDamage(patient1.WorldPosition, new List() { new Affliction(AfflictionPrefab.Burn, 15.0f) }, stun: 0, playSound: false); patient1.AIController.Enabled = false; assistantInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, "", JobPrefab.Get("assistant")); patient2 = Character.Create(assistantInfo, patientHull2.WorldPosition, "2"); - patient2.TeamID = Character.TeamType.Team1; + patient2.TeamID = CharacterTeamType.Team1; patient2.GiveJobItems(null); patient2.CanSpeak = false; patient2.AIController.Enabled = false; var mechanicInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, "", JobPrefab.Get("engineer")); var subPatient1 = Character.Create(mechanicInfo, WayPoint.GetRandom(SpawnType.Human, mechanicInfo.Job, Submarine.MainSub).WorldPosition, "3"); - subPatient1.TeamID = Character.TeamType.Team1; + subPatient1.TeamID = CharacterTeamType.Team1; subPatient1.AddDamage(patient1.WorldPosition, new List() { new Affliction(AfflictionPrefab.Burn, 40.0f) }, stun: 0, playSound: false); subPatients.Add(subPatient1); var securityInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, "", JobPrefab.Get("securityofficer")); var subPatient2 = Character.Create(securityInfo, WayPoint.GetRandom(SpawnType.Human, securityInfo.Job, Submarine.MainSub).WorldPosition, "3"); - subPatient2.TeamID = Character.TeamType.Team1; + subPatient2.TeamID = CharacterTeamType.Team1; subPatient2.AddDamage(patient1.WorldPosition, new List() { new Affliction(AfflictionPrefab.InternalDamage, 40.0f) }, stun: 0, playSound: false); subPatients.Add(subPatient2); var engineerInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, "", JobPrefab.Get("engineer")); var subPatient3 = Character.Create(securityInfo, WayPoint.GetRandom(SpawnType.Human, engineerInfo.Job, Submarine.MainSub).WorldPosition, "3"); - subPatient3.TeamID = Character.TeamType.Team1; + subPatient3.TeamID = CharacterTeamType.Team1; subPatient3.AddDamage(patient1.WorldPosition, new List() { new Affliction(AfflictionPrefab.Burn, 20.0f) }, stun: 0, playSound: false); subPatients.Add(subPatient3); @@ -200,18 +200,18 @@ namespace Barotrauma.Tutorials do { - for (int i = 0; i < doctor_suppliesCabinet.Inventory.Items.Length; i++) + for (int i = 0; i < doctor_suppliesCabinet.Inventory.Capacity; i++) { - if (doctor_suppliesCabinet.Inventory.Items[i] != null) + if (doctor_suppliesCabinet.Inventory.GetItemAt(i) != null) { HighlightInventorySlot(doctor_suppliesCabinet.Inventory, i, highlightColor, .5f, .5f, 0f); } } if (doctor.SelectedConstruction == doctor_suppliesCabinet.Item) { - for (int i = 0; i < doctor.Inventory.slots.Length; i++) + for (int i = 0; i < doctor.Inventory.Capacity; i++) { - if (doctor.Inventory.Items[i] == null) HighlightInventorySlot(doctor.Inventory, i, highlightColor, .5f, .5f, 0f); + if (doctor.Inventory.GetItemAt(i) == null) { HighlightInventorySlot(doctor.Inventory, i, highlightColor, .5f, .5f, 0f); } } } yield return null; @@ -309,16 +309,16 @@ namespace Barotrauma.Tutorials { for (int i = 0; i < 3; i++) { - if (doctor_medBayCabinet.Inventory.Items[i] != null) + if (doctor_medBayCabinet.Inventory.GetItemAt(i) != null) { HighlightInventorySlot(doctor_medBayCabinet.Inventory, i, highlightColor, .5f, .5f, 0f); } } if (doctor.SelectedConstruction == doctor_medBayCabinet.Item) { - for (int i = 0; i < doctor.Inventory.slots.Length; i++) + for (int i = 0; i < doctor.Inventory.Capacity; i++) { - if (doctor.Inventory.Items[i] == null) HighlightInventorySlot(doctor.Inventory, i, highlightColor, .5f, .5f, 0f); + if (doctor.Inventory.GetItemAt(i) == null) { HighlightInventorySlot(doctor.Inventory, i, highlightColor, .5f, .5f, 0f); } } } yield return null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs index 09c22b475..31ea40a37 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/EngineerTutorial.cs @@ -247,30 +247,30 @@ namespace Barotrauma.Tutorials if (!firstSlotRemoved) { HighlightInventorySlot(engineer_equipmentCabinet.Inventory, 0, highlightColor, .5f, .5f, 0f); - if (engineer_equipmentCabinet.Inventory.Items[0] == null) firstSlotRemoved = true; + if (engineer_equipmentCabinet.Inventory.GetItemAt(0) == null) { firstSlotRemoved = true; } } if (!secondSlotRemoved) { HighlightInventorySlot(engineer_equipmentCabinet.Inventory, 1, highlightColor, .5f, .5f, 0f); - if (engineer_equipmentCabinet.Inventory.Items[1] == null) secondSlotRemoved = true; + if (engineer_equipmentCabinet.Inventory.GetItemAt(1) == null) { secondSlotRemoved = true; } } if (!thirdSlotRemoved) { HighlightInventorySlot(engineer_equipmentCabinet.Inventory, 2, highlightColor, .5f, .5f, 0f); - if (engineer_equipmentCabinet.Inventory.Items[2] == null) thirdSlotRemoved = true; + if (engineer_equipmentCabinet.Inventory.GetItemAt(2) == null) { thirdSlotRemoved = true; } } if (!fourthSlotRemoved) { HighlightInventorySlot(engineer_equipmentCabinet.Inventory, 3, highlightColor, .5f, .5f, 0f); - if (engineer_equipmentCabinet.Inventory.Items[2] == null) fourthSlotRemoved = true; + if (engineer_equipmentCabinet.Inventory.GetItemAt(2) == null) { fourthSlotRemoved = true; } } - for (int i = 0; i < engineer.Inventory.slots.Length; i++) + for (int i = 0; i < engineer.Inventory.visualSlots.Length; i++) { - if (engineer.Inventory.Items[i] == null) HighlightInventorySlot(engineer.Inventory, i, highlightColor, .5f, .5f, 0f); + if (engineer.Inventory.GetItemAt(i) == null) { HighlightInventorySlot(engineer.Inventory, i, highlightColor, .5f, .5f, 0f); } } } @@ -299,12 +299,12 @@ namespace Barotrauma.Tutorials } while (!engineer_reactor.PowerOn); do { - if (IsSelectedItem(engineer_reactor.Item) && engineer_reactor.Item.OwnInventory.slots != null) + if (IsSelectedItem(engineer_reactor.Item) && engineer_reactor.Item.OwnInventory.visualSlots != null) { engineer_reactor.AutoTemp = false; HighlightInventorySlot(engineer.Inventory, "fuelrod", highlightColor, 0.5f, 0.5f, 0f); - for (int i = 0; i < engineer_reactor.Item.OwnInventory.slots.Length; i++) + for (int i = 0; i < engineer_reactor.Item.OwnInventory.visualSlots.Length; i++) { HighlightInventorySlot(engineer_reactor.Item.OwnInventory, i, highlightColor, 0.5f, 0.5f, 0f); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs index ea817521c..8c28a67ea 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/MechanicTutorial.cs @@ -165,21 +165,23 @@ namespace Barotrauma.Tutorials // Room 6 mechanic_divingSuitObjectiveSensor = Item.ItemList.Find(i => i.HasTag("mechanic_divingsuitobjectivesensor")).GetComponent(); mechanic_divingSuitContainer = Item.ItemList.Find(i => i.HasTag("mechanic_divingsuitcontainer")).GetComponent(); - for (int i = 0; i < mechanic_divingSuitContainer.Inventory.Items.Length; i++) + foreach (Item item in mechanic_divingSuitContainer.Inventory.AllItems) { - foreach (ItemComponent ic in mechanic_divingSuitContainer.Inventory.Items[i].Components) - { - ic.CanBePicked = true; - } - } - mechanic_oxygenContainer = Item.ItemList.Find(i => i.HasTag("mechanic_oxygencontainer")).GetComponent(); - for (int i = 0; i < mechanic_oxygenContainer.Inventory.Items.Length; i++) - { - foreach (ItemComponent ic in mechanic_oxygenContainer.Inventory.Items[i].Components) + foreach (ItemComponent ic in item.Components) { ic.CanBePicked = true; } } + + mechanic_oxygenContainer = Item.ItemList.Find(i => i.HasTag("mechanic_oxygencontainer")).GetComponent(); + foreach (Item item in mechanic_oxygenContainer.Inventory.AllItems) + { + foreach (ItemComponent ic in item.Components) + { + ic.CanBePicked = true; + } + } + tutorial_mechanicFinalDoor = Item.ItemList.Find(i => i.HasTag("tutorial_mechanicfinaldoor")).GetComponent(); tutorial_mechanicFinalDoorLight = Item.ItemList.Find(i => i.HasTag("tutorial_mechanicfinaldoorlight")).GetComponent(); @@ -266,24 +268,24 @@ namespace Barotrauma.Tutorials if (!firstSlotRemoved) { HighlightInventorySlot(mechanic_equipmentCabinet.Inventory, 0, highlightColor, .5f, .5f, 0f); - if (mechanic_equipmentCabinet.Inventory.Items[0] == null) firstSlotRemoved = true; + if (mechanic_equipmentCabinet.Inventory.GetItemAt(0) == null) { firstSlotRemoved = true; } } if (!secondSlotRemoved) { HighlightInventorySlot(mechanic_equipmentCabinet.Inventory, 1, highlightColor, .5f, .5f, 0f); - if (mechanic_equipmentCabinet.Inventory.Items[1] == null) secondSlotRemoved = true; + if (mechanic_equipmentCabinet.Inventory.GetItemAt(1) == null) { secondSlotRemoved = true; } } if (!thirdSlotRemoved) { HighlightInventorySlot(mechanic_equipmentCabinet.Inventory, 2, highlightColor, .5f, .5f, 0f); - if (mechanic_equipmentCabinet.Inventory.Items[2] == null) thirdSlotRemoved = true; + if (mechanic_equipmentCabinet.Inventory.GetItemAt(2) == null) { thirdSlotRemoved = true; } } - for (int i = 0; i < mechanic.Inventory.slots.Length; i++) + for (int i = 0; i < mechanic.Inventory.Capacity; i++) { - if (mechanic.Inventory.Items[i] == null) HighlightInventorySlot(mechanic.Inventory, i, highlightColor, .5f, .5f, 0f); + if (mechanic.Inventory.GetItemAt(i) == null) { HighlightInventorySlot(mechanic.Inventory, i, highlightColor, .5f, .5f, 0f); } } } @@ -355,16 +357,16 @@ namespace Barotrauma.Tutorials { if (mechanic.SelectedConstruction == mechanic_craftingCabinet.Item) { - for (int i = 0; i < mechanic.Inventory.slots.Length; i++) + for (int i = 0; i < mechanic.Inventory.Capacity; i++) { - if (mechanic.Inventory.Items[i] == null) HighlightInventorySlot(mechanic.Inventory, i, highlightColor, .5f, .5f, 0f); + if (mechanic.Inventory.GetItemAt(i) == null) { HighlightInventorySlot(mechanic.Inventory, i, highlightColor, .5f, .5f, 0f); } } if (mechanic.Inventory.FindItemByIdentifier("oxygentank") == null && mechanic.Inventory.FindItemByIdentifier("aluminium") == null) { - for (int i = 0; i < mechanic_craftingCabinet.Inventory.Items.Length; i++) + for (int i = 0; i < mechanic_craftingCabinet.Capacity; i++) { - Item item = mechanic_craftingCabinet.Inventory.Items[i]; + Item item = mechanic_craftingCabinet.Inventory.GetItemAt(i); if (item != null && item.prefab.Identifier == "oxygentank") { HighlightInventorySlot(mechanic_craftingCabinet.Inventory, i, highlightColor, .5f, .5f, 0f); @@ -374,9 +376,9 @@ namespace Barotrauma.Tutorials if (mechanic.Inventory.FindItemByIdentifier("sodium") == null) { - for (int i = 0; i < mechanic_craftingCabinet.Inventory.Items.Length; i++) + for (int i = 0; i < mechanic_craftingCabinet.Inventory.Capacity; i++) { - Item item = mechanic_craftingCabinet.Inventory.Items[i]; + Item item = mechanic_craftingCabinet.Inventory.GetItemAt(i); if (item != null && item.prefab.Identifier == "sodium") { HighlightInventorySlot(mechanic_craftingCabinet.Inventory, i, highlightColor, .5f, .5f, 0f); @@ -408,9 +410,9 @@ namespace Barotrauma.Tutorials { HighlightInventorySlot(mechanic_deconstructor.OutputContainer.Inventory, "aluminium", highlightColor, .5f, .5f, 0f); - for (int i = 0; i < mechanic.Inventory.slots.Length; i++) + for (int i = 0; i < mechanic.Inventory.Capacity; i++) { - if (mechanic.Inventory.Items[i] == null) HighlightInventorySlot(mechanic.Inventory, i, highlightColor, .5f, .5f, 0f); + if (mechanic.Inventory.GetItemAt(i) == null) { HighlightInventorySlot(mechanic.Inventory, i, highlightColor, .5f, .5f, 0f); } } } else @@ -418,14 +420,10 @@ namespace Barotrauma.Tutorials if (mechanic.Inventory.FindItemByIdentifier("oxygentank") != null && mechanic_deconstructor.InputContainer.Inventory.FindItemByIdentifier("oxygentank") == null) { HighlightInventorySlot(mechanic.Inventory, "oxygentank", highlightColor, .5f, .5f, 0f); - - if (mechanic_deconstructor.InputContainer.Inventory.slots != null) + for (int i = 0; i < mechanic_deconstructor.InputContainer.Inventory.Capacity; i++) { - for (int i = 0; i < mechanic_deconstructor.InputContainer.Inventory.slots.Length; i++) - { - HighlightInventorySlot(mechanic_deconstructor.InputContainer.Inventory, i, highlightColor, .5f, .5f, 0f); - } - } + HighlightInventorySlot(mechanic_deconstructor.InputContainer.Inventory, i, highlightColor, .5f, .5f, 0f); + } } if (mechanic_deconstructor.InputContainer.Inventory.FindItemByIdentifier("oxygentank") != null && !mechanic_deconstructor.IsActive) @@ -461,7 +459,7 @@ namespace Barotrauma.Tutorials { HighlightInventorySlot(mechanic_fabricator.OutputContainer.Inventory, "extinguisher", highlightColor, .5f, .5f, 0f); - /*for (int i = 0; i < mechanic.Inventory.slots.Length; i++) + /*for (int i = 0; i < mechanic.Inventory.Capacity; i++) { if (mechanic.Inventory.Items[i] == null) HighlightInventorySlot(mechanic.Inventory, i, highlightColor, .5f, .5f, 0f); }*/ @@ -478,12 +476,12 @@ namespace Barotrauma.Tutorials HighlightInventorySlot(mechanic.Inventory, "aluminium", highlightColor, .5f, .5f, 0f); HighlightInventorySlot(mechanic.Inventory, "sodium", highlightColor, .5f, .5f, 0f); - if (mechanic_fabricator.InputContainer.Inventory.Items[0] == null) + if (mechanic_fabricator.InputContainer.Inventory.GetItemAt(0) == null) { HighlightInventorySlot(mechanic_fabricator.InputContainer.Inventory, 0, highlightColor, .5f, .5f, 0f); } - if (mechanic_fabricator.InputContainer.Inventory.Items[1] == null) + if (mechanic_fabricator.InputContainer.Inventory.GetItemAt(1) == null) { HighlightInventorySlot(mechanic_fabricator.InputContainer.Inventory, 1, highlightColor, .5f, .5f, 0f); } @@ -524,9 +522,9 @@ namespace Barotrauma.Tutorials { if (IsSelectedItem(mechanic_divingSuitContainer.Item)) { - if (mechanic_divingSuitContainer.Inventory.slots != null) + if (mechanic_divingSuitContainer.Inventory.visualSlots != null) { - for (int i = 0; i < mechanic_divingSuitContainer.Inventory.slots.Length; i++) + for (int i = 0; i < mechanic_divingSuitContainer.Inventory.Capacity; i++) { HighlightInventorySlot(mechanic_divingSuitContainer.Inventory, i, highlightColor, 0.5f, 0.5f, 0f); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs index caaa10e45..e88f1e314 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs @@ -234,24 +234,24 @@ namespace Barotrauma.Tutorials if (!firstSlotRemoved) { HighlightInventorySlot(officer_equipmentCabinet.Inventory, 0, highlightColor, .5f, .5f, 0f); - if (officer_equipmentCabinet.Inventory.Items[0] == null) firstSlotRemoved = true; + if (officer_equipmentCabinet.Inventory.GetItemAt(0) == null) { firstSlotRemoved = true; } } if (!secondSlotRemoved) { HighlightInventorySlot(officer_equipmentCabinet.Inventory, 1, highlightColor, .5f, .5f, 0f); - if (officer_equipmentCabinet.Inventory.Items[1] == null) secondSlotRemoved = true; + if (officer_equipmentCabinet.Inventory.GetItemAt(1) == null) { secondSlotRemoved = true; } } if (!thirdSlotRemoved) { HighlightInventorySlot(officer_equipmentCabinet.Inventory, 2, highlightColor, .5f, .5f, 0f); - if (officer_equipmentCabinet.Inventory.Items[2] == null) thirdSlotRemoved = true; + if (officer_equipmentCabinet.Inventory.GetItemAt(2) == null) { thirdSlotRemoved = true; } } - for (int i = 0; i < officer.Inventory.slots.Length; i++) + for (int i = 0; i < officer.Inventory.visualSlots.Length; i++) { - if (officer.Inventory.Items[i] == null) HighlightInventorySlot(officer.Inventory, i, highlightColor, .5f, .5f, 0f); + if (officer.Inventory.GetItemAt(i) == null) { HighlightInventorySlot(officer.Inventory, i, highlightColor, .5f, .5f, 0f); } } } @@ -298,7 +298,7 @@ namespace Barotrauma.Tutorials TriggerTutorialSegment(3); // Arm coilgun do { - SetHighlight(officer_coilgunLoader.Item, officer_coilgunLoader.Inventory.Items[0] == null || officer_coilgunLoader.Inventory.Items[0].Condition == 0); + SetHighlight(officer_coilgunLoader.Item, officer_coilgunLoader.Inventory.GetItemAt(0) == null || officer_coilgunLoader.Inventory.GetItemAt(0).Condition == 0); HighlightInventorySlot(officer_coilgunLoader.Inventory, 0, highlightColor, .5f, .5f, 0f); SetHighlight(officer_superCapacitor.Item, officer_superCapacitor.RechargeSpeed < superCapacitorRechargeRate); SetHighlight(officer_ammoShelf_1.Item, officer_coilgunLoader.Item.ExternalHighlight ); @@ -308,7 +308,7 @@ namespace Barotrauma.Tutorials HighlightInventorySlot(officer.Inventory, "coilgunammobox", highlightColor, .5f, .5f, 0f); } yield return null; - } while (officer_coilgunLoader.Inventory.Items[0] == null || officer_superCapacitor.RechargeSpeed < superCapacitorRechargeRate || officer_coilgunLoader.Inventory.Items[0].Condition == 0); + } while (officer_coilgunLoader.Inventory.GetItemAt(0) == null || officer_superCapacitor.RechargeSpeed < superCapacitorRechargeRate || officer_coilgunLoader.Inventory.GetItemAt(0).Condition == 0); SetHighlight(officer_coilgunLoader.Item, false); SetHighlight(officer_superCapacitor.Item, false); SetHighlight(officer_ammoShelf_1.Item, false); @@ -371,12 +371,11 @@ namespace Barotrauma.Tutorials { if (IsSelectedItem(officer_rangedWeaponCabinet.Item)) { - if (officer_rangedWeaponCabinet.Inventory.slots != null) + if (officer_rangedWeaponCabinet.Inventory.visualSlots != null) { - for (int i = 0; i < officer_rangedWeaponCabinet.Inventory.Items.Length; i++) + for (int i = 0; i < officer_rangedWeaponCabinet.Inventory.Capacity; i++) { - if (officer_rangedWeaponCabinet.Inventory.Items[i] == null) continue; - if (officer_rangedWeaponCabinet.Inventory.Items[i].Prefab.Identifier == "shotgunshell") + if (officer_rangedWeaponCabinet.Inventory.GetItemAt(i)?.Prefab.Identifier == "shotgunshell") { HighlightInventorySlot(officer_rangedWeaponCabinet.Inventory, i, highlightColor, 0.5f, 0.5f, 0f); } @@ -384,10 +383,9 @@ namespace Barotrauma.Tutorials } } - for (int i = 0; i < officer.Inventory.Items.Length; i++) + for (int i = 0; i < officer.Inventory.Capacity; i++) { - if (officer.Inventory.Items[i] == null) continue; - if (officer.Inventory.Items[i].Prefab.Identifier == "shotgunshell") + if (officer.Inventory.GetItemAt(i)?.Prefab.Identifier == "shotgunshell") { HighlightInventorySlot(officer.Inventory, i, highlightColor, 0.5f, 0.5f, 0f); } @@ -398,7 +396,7 @@ namespace Barotrauma.Tutorials HighlightInventorySlot(officer.Inventory, "shotgun", highlightColor, 0.5f, 0.5f, 0f); } yield return null; - } while (!shotGunChamber.Inventory.IsFull()); // Wait until all six harpoons loaded + } while (!shotGunChamber.Inventory.IsFull(takeStacksIntoAccount: true)); // Wait until all six harpoons loaded RemoveCompletedObjective(segments[5]); SetHighlight(officer_rangedWeaponCabinet.Item, false); SetDoorAccess(officer_fourthDoor, officer_fourthDoorLight, true); @@ -425,8 +423,8 @@ namespace Barotrauma.Tutorials GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.Get("Officer.Radio.Submarine"), ChatMessageType.Radio, null); do { - SetHighlight(officer_subLoader_1.Item, officer_subLoader_1.Inventory.Items[0] == null || officer_subLoader_1.Inventory.Items[0].Condition == 0); - SetHighlight(officer_subLoader_2.Item, officer_subLoader_2.Inventory.Items[0] == null || officer_subLoader_2.Inventory.Items[0].Condition == 0); + SetHighlight(officer_subLoader_1.Item, officer_subLoader_1.Inventory.GetItemAt(0) == null || officer_subLoader_1.Inventory.GetItemAt(0).Condition == 0); + SetHighlight(officer_subLoader_2.Item, officer_subLoader_2.Inventory.GetItemAt(0) == null || officer_subLoader_2.Inventory.GetItemAt(0).Condition == 0); HighlightInventorySlot(officer_subLoader_1.Inventory, 0, highlightColor, .5f, .5f, 0f); HighlightInventorySlot(officer_subLoader_2.Inventory, 0, highlightColor, .5f, .5f, 0f); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs index 4de2994c4..c25686093 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs @@ -122,7 +122,7 @@ namespace Barotrauma.Tutorials } character = Character.Create(charInfo, wayPoint.WorldPosition, "", isRemotePlayer: false, hasAi: false); - character.TeamID = Character.TeamType.Team1; + character.TeamID = CharacterTeamType.Team1; Character.Controlled = character; character.GiveJobItems(null); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs index 1af887277..bcb0f796d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs @@ -264,7 +264,7 @@ namespace Barotrauma.Tutorials protected virtual void TriggerTutorialSegment(int index, params object[] args) { - Inventory.draggingItem = null; + Inventory.DraggingItems.Clear(); ContentRunning = true; activeContentSegment = segments[index]; segments[index].Args = args; @@ -410,7 +410,7 @@ namespace Barotrauma.Tutorials private void ReplaySegmentVideo(TutorialSegment segment) { if (ContentRunning) return; - Inventory.draggingItem = null; + Inventory.DraggingItems.Clear(); ContentRunning = true; LoadVideo(segment); //videoPlayer.LoadContent(playableContentPath, new VideoPlayer.VideoSettings(segment.VideoContent), new VideoPlayer.TextSettings(segment.VideoContent), segment.Id, true, callback: () => ContentRunning = false); @@ -419,7 +419,7 @@ namespace Barotrauma.Tutorials private void ShowSegmentText(TutorialSegment segment) { if (ContentRunning) return; - Inventory.draggingItem = null; + Inventory.DraggingItems.Clear(); ContentRunning = true; string tutorialText = TextManager.GetFormatted(segment.TextContent.GetAttributeString("tag", ""), true, segment.Args); @@ -609,10 +609,10 @@ namespace Barotrauma.Tutorials #region Highlights protected void HighlightInventorySlot(Inventory inventory, string identifier, Color color, float fadeInDuration, float fadeOutDuration, float scaleUpAmount) { - if (inventory.slots == null) { return; } - for (int i = 0; i < inventory.Items.Length; i++) + if (inventory.visualSlots == null) { return; } + for (int i = 0; i < inventory.Capacity; i++) { - if (inventory.Items[i] != null && inventory.Items[i].Prefab.Identifier == identifier) + if (inventory.GetItemAt(i)?.Prefab.Identifier == identifier) { HighlightInventorySlot(inventory, i, color, fadeInDuration, fadeOutDuration, scaleUpAmount); } @@ -621,10 +621,10 @@ namespace Barotrauma.Tutorials protected void HighlightInventorySlotWithTag(Inventory inventory, string tag, Color color, float fadeInDuration, float fadeOutDuration, float scaleUpAmount) { - if (inventory.slots == null) { return; } - for (int i = 0; i < inventory.Items.Length; i++) + if (inventory.visualSlots == null) { return; } + for (int i = 0; i < inventory.Capacity; i++) { - if (inventory.Items[i] != null && inventory.Items[i].HasTag(tag)) + if (inventory.GetItemAt(i)?.HasTag(tag) ?? false) { HighlightInventorySlot(inventory, i, color, fadeInDuration, fadeOutDuration, scaleUpAmount); } @@ -633,8 +633,8 @@ namespace Barotrauma.Tutorials protected void HighlightInventorySlot(Inventory inventory, int index, Color color, float fadeInDuration, float fadeOutDuration, float scaleUpAmount) { - if (inventory.slots == null || index < 0 || inventory.slots[index].HighlightTimer > 0) return; - inventory.slots[index].ShowBorderHighlight(color, fadeInDuration, fadeOutDuration, scaleUpAmount); + if (inventory.visualSlots == null || index < 0 || inventory.visualSlots[index].HighlightTimer > 0) { return; } + inventory.visualSlots[index].ShowBorderHighlight(color, fadeInDuration, fadeOutDuration, scaleUpAmount); } #endregion } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs index cdb324954..71ffe721d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs @@ -33,6 +33,8 @@ namespace Barotrauma public static DateTime lastReadyCheck = DateTime.MinValue; + public static bool IsReadyCheck(GUIComponent? msgBox) => msgBox?.UserData as string == PromptData || msgBox?.UserData as string == ResultData; + private void CreateMessageBox(string author) { Vector2 relativeSize = new Vector2(GUI.IsFourByThree() ? 0.3f : 0.2f, 0.15f); @@ -120,7 +122,10 @@ namespace Barotrauma int second = (int) Math.Ceiling(time); if (second < lastSecond) { - SoundPlayer.PlayUISound(GUISoundType.PopupMenu); + if (msgBox != null && !msgBox.Closed) + { + SoundPlayer.PlayUISound(GUISoundType.PopupMenu); + } lastSecond = second; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs index 4a9530609..5b0dd6f58 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs @@ -87,12 +87,12 @@ namespace Barotrauma TextManager.Get("crew"), textAlignment: Alignment.TopLeft, font: GUI.SubHeadingFont); crewHeader.RectTransform.MinSize = new Point(0, GUI.IntScale(crewHeader.Rect.Height * 2.0f)); - CreateCrewList(crewContent, gameSession.CrewManager.GetCharacterInfos().Where(c => c.TeamID != Character.TeamType.Team2)); + CreateCrewList(crewContent, gameSession.CrewManager.GetCharacterInfos().Where(c => c.TeamID != CharacterTeamType.Team2)); //another crew frame for the 2nd team in combat missions if (gameSession.Mission is CombatMission) { - crewHeader.Text = CombatMission.GetTeamName(Character.TeamType.Team1); + crewHeader.Text = CombatMission.GetTeamName(CharacterTeamType.Team1); GUIFrame crewFrame2 = new GUIFrame(new RectTransform(new Vector2(0.35f, 0.55f), background.RectTransform, Anchor.TopCenter, minSize: new Point(minWidth, minHeight))); rightPanels.Add(crewFrame2); GUIFrame crewFrameInner2 = new GUIFrame(new RectTransform(new Point(crewFrame2.Rect.Width - padding * 2, crewFrame2.Rect.Height - padding * 2), crewFrame2.RectTransform, Anchor.Center), style: "InnerFrame"); @@ -101,9 +101,9 @@ namespace Barotrauma Stretch = true }; var crewHeader2 = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), crewContent2.RectTransform), - CombatMission.GetTeamName(Character.TeamType.Team2), textAlignment: Alignment.TopLeft, font: GUI.SubHeadingFont); + CombatMission.GetTeamName(CharacterTeamType.Team2), textAlignment: Alignment.TopLeft, font: GUI.SubHeadingFont); crewHeader2.RectTransform.MinSize = new Point(0, GUI.IntScale(crewHeader2.Rect.Height * 2.0f)); - CreateCrewList(crewContent2, gameSession.CrewManager.GetCharacterInfos().Where(c => c.TeamID == Character.TeamType.Team2)); + CreateCrewList(crewContent2, gameSession.CrewManager.GetCharacterInfos().Where(c => c.TeamID == CharacterTeamType.Team2)); } //header ------------------------------------------------------------------------------- diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs index e2861f84d..6e9c0b744 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs @@ -68,6 +68,9 @@ namespace Barotrauma keyMapping[(int)InputType.PreviousFireMode] = new KeyOrMouse(MouseButton.MouseWheelDown); keyMapping[(int)InputType.NextFireMode] = new KeyOrMouse(MouseButton.MouseWheelUp); + keyMapping[(int)InputType.TakeHalfFromInventorySlot] = new KeyOrMouse(Keys.LeftShift); + keyMapping[(int)InputType.TakeOneFromInventorySlot] = new KeyOrMouse(Keys.LeftControl); + if (Language == "French") { keyMapping[(int)InputType.Up] = new KeyOrMouse(Keys.Z); @@ -173,6 +176,13 @@ namespace Barotrauma { foreach (XAttribute attribute in element.Attributes()) { + //backwards compatibility + if (attribute.Name.ToString() == "TakeAllFromInventorySlot") + { + keyMapping[(int)InputType.TakeHalfFromInventorySlot] = new KeyOrMouse(Keys.LeftShift); + keyMapping[(int)InputType.TakeOneFromInventorySlot] = new KeyOrMouse(Keys.LeftControl); + } + if (!Enum.TryParse(attribute.Name.ToString(), true, out InputType inputType)) { continue; } if (int.TryParse(attribute.Value.ToString(), out int mouseButtonInt)) @@ -223,6 +233,40 @@ namespace Barotrauma { LoadInventoryKeybinds(inventoryKeyMapping); } + + XElement debugConsoleMapping = doc.Root.Element("debugconsolemapping"); + + if (debugConsoleMapping == null) { return; } + + ConsoleKeybinds.Clear(); + DebugConsole.Keybinds.Clear(); + + foreach (XElement element in debugConsoleMapping.Elements()) + { + string keyString = element.GetAttributeString("key", string.Empty); + string command = element.GetAttributeString("command", string.Empty); + + if (string.IsNullOrWhiteSpace(keyString) || string.IsNullOrWhiteSpace(command)) { continue; } + + if (Enum.TryParse(typeof(Keys), keyString, ignoreCase: true, out object @out) && @out is Keys key) + { + ConsoleKeybinds.TryAdd(key, command); + } + } + + DebugConsole.Keybinds = new Dictionary(ConsoleKeybinds); + } + + private void LoadSubEditorImages(XDocument doc) + { + XElement element = doc.Root?.Element("editorimages"); + if (element == null) + { + SubEditorScreen.ImageManager.Clear(alsoPending: true); + return; + } + + SubEditorScreen.ImageManager.Load(element); } public KeyOrMouse KeyBind(InputType inputType) @@ -242,7 +286,7 @@ namespace Barotrauma private GUIListBox contentPackageList; - private bool ChangeSliderText(GUIScrollBar scrollBar, float barScroll) + private bool ChangeSliderText(GUIScrollBar scrollBar, float scale) { UnsavedSettings = true; GUITextBlock text = scrollBar.UserData as GUITextBlock; @@ -261,7 +305,7 @@ namespace Barotrauma } label = text.Text.Substring(0, index); } - text.Text = label + " " + (int)(barScroll * 100) + "%"; + text.Text = label + " " + (int)Math.Round(scale * 100) + "%"; return true; } @@ -705,6 +749,18 @@ namespace Barotrauma } };*/ + new GUITickBox(new RectTransform(tickBoxScale, rightColumn.RectTransform), TextManager.Get("RadialDistortion")) + { + ToolTip = TextManager.Get("RadialDistortionToolTip"), + Selected = EnableRadialDistortion, + OnSelected = (tickBox) => + { + EnableRadialDistortion = tickBox.Selected; + UnsavedSettings = true; + return true; + } + }; + new GUITickBox(new RectTransform(tickBoxScale, rightColumn.RectTransform), TextManager.Get("ChromaticAberration")) { ToolTip = TextManager.Get("ChromaticAberrationToolTip"), @@ -725,13 +781,12 @@ namespace Barotrauma BarScroll = (HUDScale - MinHUDScale) / (MaxHUDScale - MinHUDScale), OnMoved = (scrollBar, scroll) => { - ChangeSliderText(scrollBar, scroll); HUDScale = MathHelper.Lerp(MinHUDScale, MaxHUDScale, scroll); - UnsavedSettings = true; + ChangeSliderText(scrollBar, HUDScale); OnHUDScaleChanged?.Invoke(); return true; }, - Step = 0.05f + Step = 0.02f }; HUDScaleScrollBar.OnMoved(HUDScaleScrollBar, HUDScaleScrollBar.BarScroll); @@ -743,15 +798,31 @@ namespace Barotrauma BarScroll = (InventoryScale - MinInventoryScale) / (MaxInventoryScale - MinInventoryScale), OnMoved = (scrollBar, scroll) => { - ChangeSliderText(scrollBar, scroll); InventoryScale = MathHelper.Lerp(MinInventoryScale, MaxInventoryScale, scroll); - UnsavedSettings = true; + ChangeSliderText(scrollBar, InventoryScale); return true; }, - Step = 0.05f + Step = 0.02f }; inventoryScaleScrollBar.OnMoved(inventoryScaleScrollBar, inventoryScaleScrollBar.BarScroll); + GUITextBlock textScaleText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), rightColumn.RectTransform), TextManager.Get("TextScale"), font: GUI.SubHeadingFont); + GUIScrollBar textScaleScrollBar = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.05f), rightColumn.RectTransform), + style: "GUISlider", barSize: 0.1f) + { + UserData = textScaleText, + BarScroll = (TextScale - MinTextScale) / (MaxTextScale - MinTextScale), + OnMoved = (scrollBar, scroll) => + { + TextScale = MathHelper.Lerp(MinTextScale, MaxTextScale, scroll); + textScaleDirty = true; + ChangeSliderText(scrollBar, TextScale); + return true; + }, + Step = 0.01f + }; + textScaleScrollBar.OnMoved(textScaleScrollBar, textScaleScrollBar.BarScroll); + /// Audio tab ---------------------------------------------------------------- var audioContent = new GUILayoutGroup(new RectTransform(new Vector2(0.97f, 0.97f), tabs[(int)Tab.Audio].RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) @@ -809,8 +880,7 @@ namespace Barotrauma ChangeSliderText(scrollBar, scroll); SoundVolume = scroll; return true; - }, - Step = 0.05f + } }; soundScrollBar.OnMoved(soundScrollBar, soundScrollBar.BarScroll); @@ -825,8 +895,7 @@ namespace Barotrauma ChangeSliderText(scrollBar, scroll); MusicVolume = scroll; return true; - }, - Step = 0.05f + } }; musicScrollBar.OnMoved(musicScrollBar, musicScrollBar.BarScroll); @@ -835,8 +904,7 @@ namespace Barotrauma style: "GUISlider", barSize: 0.05f) { UserData = voiceChatVolumeText, - Range = new Vector2(0.0f, 2.0f), - Step = 0.05f + Range = new Vector2(0.0f, 2.0f) }; voiceChatScrollBar.BarScrollValue = VoiceChatVolume; voiceChatScrollBar.OnMoved = (scrollBar, scroll) => @@ -1014,6 +1082,19 @@ namespace Barotrauma { Visible = VoiceSetting != VoiceMode.Disabled }; + GUITickBox localVoiceByDefault = new GUITickBox( + new RectTransform(tickBoxScale, voiceActivityGroup.RectTransform), TextManager.Get("LocalVoiceByDefault")) + { + Visible = VoiceSetting == VoiceMode.Activity, + Selected = UseLocalVoiceByDefault, + ToolTip = TextManager.Get("LocalVoiceByDefaultTooltip"), + OnSelected = (tickBox) => + { + UseLocalVoiceByDefault = tickBox.Selected; + UnsavedSettings = true; + return true; + } + }; GUITextBlock noiseGateText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), voiceActivityGroup.RectTransform), TextManager.Get("NoiseGateThreshold"), font: GUI.SubHeadingFont) { Visible = VoiceSetting == VoiceMode.Activity, @@ -1140,6 +1221,7 @@ namespace Barotrauma noiseGateText.Visible = (vMode == VoiceMode.Activity); noiseGateSlider.Visible = (vMode == VoiceMode.Activity); + localVoiceByDefault.Visible = (vMode == VoiceMode.Activity); voiceActivityGroup.Visible = (vMode != VoiceMode.Disabled); voiceInputContainerHorizontal.Visible = (vMode == VoiceMode.PushToTalk); UnsavedSettings = true; @@ -1183,7 +1265,7 @@ namespace Barotrauma AimAssistAmount = MathHelper.Lerp(0.0f, 5.0f, scroll); return true; }, - Step = 0.1f + Step = 0.01f }; aimAssistSlider.OnMoved(aimAssistSlider, aimAssistSlider.BarScroll); @@ -1199,19 +1281,21 @@ namespace Barotrauma } }; - var inputFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.75f), controlsLayoutGroup.RectTransform), isHorizontal: true) - { Stretch = true, RelativeSpacing = 0.03f }; + var controlListBox = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.75f), controlsLayoutGroup.RectTransform)); + + var inputFrame = new GUILayoutGroup(new RectTransform(Vector2.One, controlListBox.Content.RectTransform), isHorizontal: true) + { Stretch = true, RelativeSpacing = 0.01f }; var inputColumnLeft = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), inputFrame.RectTransform)) - { Stretch = true, RelativeSpacing = 0.02f }; + { Stretch = true, RelativeSpacing = 0.005f }; var inputColumnRight = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), inputFrame.RectTransform)) - { Stretch = true, RelativeSpacing = 0.02f }; + { Stretch = true, RelativeSpacing = 0.005f }; var inputNames = Enum.GetValues(typeof(InputType)); var inputNameBlocks = new List(); for (int i = 0; i < inputNames.Length; i++) { - var inputContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.06f),(i <= (inputNames.Length / 2.2f) ? inputColumnLeft : inputColumnRight).RectTransform)) + var inputContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.06f),(i <= (inputNames.Length / 2) ? inputColumnLeft : inputColumnRight).RectTransform)) { Stretch = true, IsHorizontal = true, RelativeSpacing = 0.01f, Color = new Color(12, 14, 15, 215) }; var inputName = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), inputContainer.RectTransform, Anchor.TopLeft) { MinSize = new Point(100, 0) }, TextManager.Get("InputType." + ((InputType)i)), font: GUI.SmallFont) { ForceUpperCase = true }; @@ -1226,14 +1310,17 @@ namespace Barotrauma { keyBox.Text = ToolBox.LimitString(keyText, keyBox.Font, (int)(keyBox.Rect.Width - keyBox.Padding.X - keyBox.Padding.Z)); }; + inputContainer.RectTransform.MinSize = keyBox.RectTransform.MinSize; keyBox.OnSelected += KeyBoxSelected; keyBox.SelectedColor = Color.Gold * 0.3f; } + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.06f), inputColumnRight.RectTransform, minSize: inputColumnRight.Children.First().RectTransform.MinSize), style: null); + for (int i = 0; i < inventoryHotkeyCount; i++) { var inputContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.06f), ((i + 1) <= inventoryHotkeyCount / 2 ? inputColumnLeft : inputColumnRight).RectTransform)) - { Stretch = true, IsHorizontal = true, RelativeSpacing = 0.01f, Color = new Color(12, 14, 15, 215) }; + { Stretch = true, IsHorizontal = true, RelativeSpacing = 0.01f, Color = new Color(12, 14, 15, 215), CanBeFocused = true }; var inputName = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), inputContainer.RectTransform, Anchor.TopLeft) { MinSize = new Point(100, 0) }, TextManager.GetWithVariable("inventoryslotkeybind", "[slotnumber]", (i + 1).ToString()), font: GUI.SmallFont) { ForceUpperCase = true }; @@ -1244,11 +1331,20 @@ namespace Barotrauma UserData = i }; keyBox.Text = ToolBox.LimitString(keyBox.Text, keyBox.Font, (int)(keyBox.Rect.Width - keyBox.Padding.X - keyBox.Padding.Z)); + inputContainer.RectTransform.MinSize = keyBox.RectTransform.MinSize; keyBox.OnSelected += InventoryKeyBoxSelected; keyBox.SelectedColor = Color.Gold * 0.3f; } - GUITextBlock.AutoScaleAndNormalize(inputNameBlocks); + inputNameBlocks.First().RectTransform.SizeChanged += () => + { + GUITextBlock.AutoScaleAndNormalize(inputNameBlocks); + }; + + inputFrame.RectTransform.MinSize = new Point(0, + (int)Math.Max( + inputColumnLeft.Children.Sum(c => c.Rect.Height * (1.0f + inputColumnLeft.RelativeSpacing)), + inputColumnRight.Children.Sum(c => c.Rect.Height * (1.0f + inputColumnLeft.RelativeSpacing)))); var resetControlsArea = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.07f), controlsLayoutGroup.RectTransform), style: null); var resetControlsHolder = new GUILayoutGroup(new RectTransform(new Vector2(buttonArea.RectTransform.RelativeSize.X / controlsLayoutGroup.RectTransform.RelativeSize.X / rightPanel.RectTransform.RelativeSize.X, 1.0f), resetControlsArea.RectTransform, Anchor.Center), @@ -1597,7 +1693,7 @@ namespace Barotrauma if (!EnabledRegularPackages.Contains(contentPackage)) { return; } } - ContentPackage.SortContentPackages(cp => listBox.Content.GetChildIndex(listBox.Content.GetChildByUserData(cp)), true); + ContentPackage.SortContentPackages(cp => listBox.Content.GetChildIndex(listBox.Content.GetChildByUserData(cp)), true, this); UnsavedSettings = true; } @@ -1616,7 +1712,9 @@ namespace Barotrauma { DisableRegularPackage(contentPackage); } - + + ContentPackage.SortContentPackages(cp => contentPackageList.Content.GetChildIndex(contentPackageList.Content.GetChildByUserData(cp)), false, this); + UnsavedSettings = true; return true; } @@ -1707,22 +1805,11 @@ namespace Barotrauma SettingsFrame.Flash(GUI.Style.Green); - if (GameMain.WindowMode != GameMain.Config.WindowMode || GameMain.Config.GraphicsWidth != GameMain.GraphicsWidth || GameMain.Config.GraphicsHeight != GameMain.GraphicsHeight) + if (textScaleDirty || GameMain.WindowMode != GameMain.Config.WindowMode || GameMain.Config.GraphicsWidth != GameMain.GraphicsWidth || GameMain.Config.GraphicsHeight != GameMain.GraphicsHeight) { GameMain.Instance.ApplyGraphicsSettings(); + textScaleDirty = false; } - - /*if (GameMain.GraphicsWidth != GameMain.Config.GraphicsWidth || GameMain.GraphicsHeight != GameMain.Config.GraphicsHeight) - { -#if OSX - if (GameMain.Config.WindowMode != WindowMode.BorderlessWindowed) - { -#endif - new GUIMessageBox(TextManager.Get("RestartRequiredLabel"), TextManager.Get("RestartRequiredResolution")); -#if OSX - } -#endif - }*/ } private bool ApplyClicked(GUIButton button, object userData) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index 850a0a7c1..bc26ebca2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -94,8 +94,8 @@ namespace Barotrauma get { return personalSlotArea; } } - private GUIImage[] indicators = new GUIImage[5]; - private int[] indicatorIndexes = new int[5]; + private readonly GUIImage[] indicators = new GUIImage[5]; + private readonly int[] indicatorIndices = new int[5]; private Vector2 indicatorSpriteSize; private GUILayoutGroup indicatorGroup; @@ -117,11 +117,11 @@ namespace Barotrauma indicators[3] = new GUIImage(new RectTransform(Point.Zero, indicatorGroup.RectTransform), "EquipmentIndicatorHeadwear"); indicators[4] = new GUIImage(new RectTransform(Point.Zero, indicatorGroup.RectTransform), "EquipmentIndicatorHeadphones"); - indicatorIndexes[0] = FindLimbSlot(InvSlotType.OuterClothes); - indicatorIndexes[1] = FindLimbSlot(InvSlotType.Card); - indicatorIndexes[2] = FindLimbSlot(InvSlotType.InnerClothes); - indicatorIndexes[3] = FindLimbSlot(InvSlotType.Head); - indicatorIndexes[4] = FindLimbSlot(InvSlotType.Headset); + indicatorIndices[0] = FindLimbSlot(InvSlotType.OuterClothes); + indicatorIndices[1] = FindLimbSlot(InvSlotType.Card); + indicatorIndices[2] = FindLimbSlot(InvSlotType.InnerClothes); + indicatorIndices[3] = FindLimbSlot(InvSlotType.Head); + indicatorIndices[4] = FindLimbSlot(InvSlotType.Headset); for (int i = 0; i < indicators.Length; i++) { @@ -143,7 +143,7 @@ namespace Barotrauma protected override ItemInventory GetActiveEquippedSubInventory(int slotIndex) { - var item = Items[slotIndex]; + Item item = slots[slotIndex].FirstOrDefault(); if (item == null) { return null; } var container = item.GetComponent(); @@ -162,38 +162,35 @@ namespace Barotrauma public override void RemoveItem(Item item) { - if (!Items.Contains(item)) { return; } + if (!Contains(item)) { return; } base.RemoveItem(item); CreateSlots(); } public override void CreateSlots() { - if (slots == null) { slots = new InventorySlot[capacity]; } + if (visualSlots == null) { visualSlots = new VisualSlot[capacity]; } float multiplier = !GUI.IsFourByThree() ? UIScale : UIScale * 0.925f; for (int i = 0; i < capacity; i++) { - InventorySlot prevSlot = slots[i]; + VisualSlot prevSlot = visualSlots[i]; Sprite slotSprite = SlotSpriteSmall; Rectangle slotRect = new Rectangle( - (int)(SlotPositions[i].X), - (int)(SlotPositions[i].Y), + (int)SlotPositions[i].X, + (int)SlotPositions[i].Y, (int)(slotSprite.size.X * multiplier), (int)(slotSprite.size.Y * multiplier)); - - if (Items[i] != null) + + ItemContainer itemContainer = slots[i].FirstOrDefault()?.GetComponent(); + if (itemContainer != null) { - ItemContainer itemContainer = Items[i].GetComponent(); - if (itemContainer != null) - { - if (itemContainer.InventoryTopSprite != null) slotRect.Width = Math.Max(slotRect.Width, (int)(itemContainer.InventoryTopSprite.size.X * UIScale)); - if (itemContainer.InventoryBottomSprite != null) slotRect.Width = Math.Max(slotRect.Width, (int)(itemContainer.InventoryBottomSprite.size.X * UIScale)); - } - } + if (itemContainer.InventoryTopSprite != null) slotRect.Width = Math.Max(slotRect.Width, (int)(itemContainer.InventoryTopSprite.size.X * UIScale)); + if (itemContainer.InventoryBottomSprite != null) slotRect.Width = Math.Max(slotRect.Width, (int)(itemContainer.InventoryBottomSprite.size.X * UIScale)); + } - slots[i] = new InventorySlot(slotRect) + visualSlots[i] = new VisualSlot(slotRect) { SubInventoryDir = Math.Sign(GameMain.GraphicsHeight / 2 - slotRect.Center.Y), Disabled = false, @@ -202,13 +199,13 @@ namespace Barotrauma }; if (prevSlot != null) { - slots[i].DrawOffset = prevSlot.DrawOffset; - slots[i].Color = prevSlot.Color; + visualSlots[i].DrawOffset = prevSlot.DrawOffset; + visualSlots[i].Color = prevSlot.Color; + prevSlot.MoveBorderHighlight(visualSlots[i]); } - if (selectedSlot?.ParentInventory == this && selectedSlot.SlotIndex == i) { - selectedSlot = new SlotReference(this, slots[i], i, selectedSlot.IsSubSlot, selectedSlot.Inventory); + selectedSlot = new SlotReference(this, visualSlots[i], i, selectedSlot.IsSubSlot, selectedSlot.Inventory); } } @@ -217,9 +214,9 @@ namespace Barotrauma highlightedSubInventorySlots.RemoveWhere(s => s.Inventory.OpenState <= 0.0f); foreach (var subSlot in highlightedSubInventorySlots) { - if (subSlot.ParentInventory == this && subSlot.SlotIndex > 0 && subSlot.SlotIndex < slots.Length) + if (subSlot.ParentInventory == this && subSlot.SlotIndex > 0 && subSlot.SlotIndex < visualSlots.Length) { - subSlot.Slot = slots[subSlot.SlotIndex]; + subSlot.Slot = visualSlots[subSlot.SlotIndex]; } } @@ -235,10 +232,10 @@ namespace Barotrauma if (HideSlot(i)) continue; if (frame == Rectangle.Empty) { - frame = slots[i].Rect; + frame = visualSlots[i].Rect; continue; } - frame = Rectangle.Union(frame, slots[i].Rect); + frame = Rectangle.Union(frame, visualSlots[i].Rect); } frame.Inflate(10, 30); frame.Location -= new Point(0, 25); @@ -247,26 +244,25 @@ namespace Barotrauma protected override bool HideSlot(int i) { - if (slots[i].Disabled || (hideEmptySlot[i] && Items[i] == null)) return true; + if (visualSlots[i].Disabled || (slots[i].HideIfEmpty && slots[i].Empty())) { return true; } if (layout == Layout.Default) { - if (PersonalSlots.HasFlag(SlotTypes[i]) && !personalSlotArea.Contains(slots[i].Rect.Center + slots[i].DrawOffset.ToPoint())) return true; + if (PersonalSlots.HasFlag(SlotTypes[i]) && !personalSlotArea.Contains(visualSlots[i].Rect.Center + visualSlots[i].DrawOffset.ToPoint())) { return true; } } + Item item = slots[i].FirstOrDefault(); + //no need to draw the right hand slot if the item is in both hands - if (Items[i] != null && SlotTypes[i] == InvSlotType.RightHand && IsInLimbSlot(Items[i], InvSlotType.LeftHand)) + if (item != null && SlotTypes[i] == InvSlotType.RightHand && IsInLimbSlot(item, InvSlotType.LeftHand)) { return true; } - //don't show the equip slot if the item is also in the default inventory - if (SlotTypes[i] != InvSlotType.Any && Items[i] != null) + //don't show the limb-specific slot if the item is also in an Any slot + if (item != null && SlotTypes[i] != InvSlotType.Any) { - for (int j = 0; j < capacity; j++) - { - if (SlotTypes[j] == InvSlotType.Any && Items[j] == Items[i]) return true; - } + if (IsInLimbSlot(item, InvSlotType.Any)) { return true; } } return false; @@ -308,7 +304,8 @@ namespace Barotrauma SlotSize = !isFourByThree ? (SlotSpriteSmall.size * UIScale).ToPoint() : (SlotSpriteSmall.size * UIScale * .925f).ToPoint(); int bottomOffset = SlotSize.Y + Spacing * 2 + ContainedIndicatorHeight; - if (slots == null) { CreateSlots(); } + if (visualSlots == null) { CreateSlots(); } + if (visualSlots.None()) { return; } hideButton.Visible = false; @@ -359,7 +356,7 @@ namespace Barotrauma { int x = HUDLayoutSettings.InventoryAreaLower.Right; int personalSlotX = HUDLayoutSettings.InventoryAreaLower.Right - SlotSize.X - Spacing; - for (int i = 0; i < slots.Length; i++) + for (int i = 0; i < visualSlots.Length; i++) { if (HideSlot(i)) continue; if (PersonalSlots.HasFlag(SlotTypes[i])) @@ -380,12 +377,12 @@ namespace Barotrauma if (PersonalSlots.HasFlag(SlotTypes[i])) { SlotPositions[i] = new Vector2(personalSlotX, personalSlotY); - personalSlotX -= slots[i].Rect.Width + Spacing; + personalSlotX -= visualSlots[i].Rect.Width + Spacing; } else { SlotPositions[i] = new Vector2(x, GameMain.GraphicsHeight - bottomOffset); - x += slots[i].Rect.Width + Spacing; + x += visualSlots[i].Rect.Width + Spacing; } } @@ -393,7 +390,7 @@ namespace Barotrauma for (int i = 0; i < SlotPositions.Length; i++) { if (!HideSlot(i)) continue; - x -= slots[i].Rect.Width + Spacing; + x -= visualSlots[i].Rect.Width + Spacing; SlotPositions[i] = new Vector2(x, GameMain.GraphicsHeight - bottomOffset); } } @@ -410,19 +407,19 @@ namespace Barotrauma if (PersonalSlots.HasFlag(SlotTypes[i])) { SlotPositions[i] = new Vector2(personalSlotX, personalSlotY); - personalSlotX += slots[i].Rect.Width + Spacing; + personalSlotX += visualSlots[i].Rect.Width + Spacing; } else { SlotPositions[i] = new Vector2(x, GameMain.GraphicsHeight - bottomOffset); - x += slots[i].Rect.Width + Spacing; + x += visualSlots[i].Rect.Width + Spacing; } } for (int i = 0; i < SlotPositions.Length; i++) { if (!HideSlot(i)) continue; SlotPositions[i] = new Vector2(x, GameMain.GraphicsHeight - bottomOffset); - x += slots[i].Rect.Width + Spacing; + x += visualSlots[i].Rect.Width + Spacing; } } break; @@ -438,10 +435,10 @@ namespace Barotrauma if (SlotTypes[i] == InvSlotType.Card || SlotTypes[i] == InvSlotType.Headset || SlotTypes[i] == InvSlotType.InnerClothes) { SlotPositions[i] = new Vector2(x, y); - x += slots[i].Rect.Width + Spacing; + x += visualSlots[i].Rect.Width + Spacing; } } - y += slots[0].Rect.Height + Spacing + ContainedIndicatorHeight + slots[0].EquipButtonRect.Height; + y += visualSlots[0].Rect.Height + Spacing + ContainedIndicatorHeight + visualSlots[0].EquipButtonRect.Height; x = startX; int n = 0; for (int i = 0; i < SlotPositions.Length; i++) @@ -450,12 +447,12 @@ namespace Barotrauma if (SlotTypes[i] != InvSlotType.Card && SlotTypes[i] != InvSlotType.Headset && SlotTypes[i] != InvSlotType.InnerClothes) { SlotPositions[i] = new Vector2(x, y); - x += slots[i].Rect.Width + Spacing; + x += visualSlots[i].Rect.Width + Spacing; n++; if (n >= columns) { x = startX; - y += slots[i].Rect.Height + Spacing + ContainedIndicatorHeight + slots[i].EquipButtonRect.Height; + y += visualSlots[i].Rect.Height + Spacing + ContainedIndicatorHeight + visualSlots[i].EquipButtonRect.Height; n = 0; } } @@ -467,7 +464,7 @@ namespace Barotrauma CreateSlots(); if (layout == Layout.Default) { - HUDLayoutSettings.InventoryTopY = slots[0].EquipButtonRect.Y - (int)(15 * GUI.Scale); + HUDLayoutSettings.InventoryTopY = visualSlots[0].EquipButtonRect.Y - (int)(15 * GUI.Scale); } } @@ -484,7 +481,8 @@ namespace Barotrauma public override void Update(float deltaTime, Camera cam, bool isSubInventory = false) { - if (!AccessibleWhenAlive && !character.IsDead) + // Need to update the infiltrator's inventory because they use id cards to access the sub. TODO: We don't probably need to update everything. + if (!AccessibleWhenAlive && !character.IsDead && (character.Params.AI == null || !character.Params.AI.Infiltrate)) { syncItemsDelay = Math.Max(syncItemsDelay - deltaTime, 0.0f); return; @@ -493,7 +491,7 @@ namespace Barotrauma base.Update(deltaTime, cam); bool hoverOnInventory = GUI.MouseOn == null && - ((selectedSlot != null && selectedSlot.IsSubSlot) || (draggingItem != null && (draggingSlot == null || !draggingSlot.MouseOn()))); + ((selectedSlot != null && selectedSlot.IsSubSlot) || (DraggingItems.Any() && (DraggingSlot == null || !DraggingSlot.MouseOn()))); if (CharacterHealth.OpenHealthWindow != null) hoverOnInventory = true; if (layout == Layout.Default && (Screen.Selected != GameMain.SubEditorScreen || Screen.Selected is SubEditorScreen editor && editor.WiringMode)) @@ -508,16 +506,16 @@ namespace Barotrauma Math.Max(hidePersonalSlotsState - deltaTime * 5.0f, 0.0f); bool personalSlotsMoving = hidePersonalSlotsState > 0 && hidePersonalSlotsState < 1f; - for (int i = 0; i < slots.Length; i++) + for (int i = 0; i < visualSlots.Length; i++) { if (!PersonalSlots.HasFlag(SlotTypes[i])) { continue; } if (HidePersonalSlots) { - if (selectedSlot?.Slot == slots[i]) { selectedSlot = null; } - highlightedSubInventorySlots.RemoveWhere(s => s.Slot == slots[i]); + if (selectedSlot?.Slot == visualSlots[i]) { selectedSlot = null; } + highlightedSubInventorySlots.RemoveWhere(s => s.Slot == visualSlots[i]); } - slots[i].IsMoving = personalSlotsMoving; - slots[i].DrawOffset = Vector2.Lerp(Vector2.Zero, new Vector2(personalSlotArea.Width, 0.0f), hidePersonalSlotsState); + visualSlots[i].IsMoving = personalSlotsMoving; + visualSlots[i].DrawOffset = Vector2.Lerp(Vector2.Zero, new Vector2(personalSlotArea.Width, 0.0f), hidePersonalSlotsState); } } } @@ -530,13 +528,15 @@ namespace Barotrauma //force personal slots open if an item is running out of battery/fuel/oxygen/etc if (hidePersonalSlots) { - for (int i = 0; i < slots.Length; i++) + for (int i = 0; i < visualSlots.Length; i++) { - if (Items[i]?.OwnInventory != null && Items[i].OwnInventory.Capacity == 1 && PersonalSlots.HasFlag(SlotTypes[i])) + var item = slots[i].FirstOrDefault(); + if (item?.OwnInventory != null && item.OwnInventory.Capacity == 1 && PersonalSlots.HasFlag(SlotTypes[i])) { - if (Items[i].OwnInventory.Items[0] != null && - Items[i].OwnInventory.Items[0].Condition > 0.0f && - Items[i].OwnInventory.Items[0].Condition / Items[i].OwnInventory.Items[0].MaxCondition < 0.15f) + var containedItem = item.OwnInventory.AllItems.FirstOrDefault(); + if (containedItem != null && + containedItem.Condition > 0.0f && + containedItem.Condition / containedItem.MaxCondition < 0.15f) { hidePersonalSlots = false; } @@ -547,7 +547,7 @@ namespace Barotrauma List hideSubInventories = new List(); highlightedSubInventorySlots.RemoveWhere(s => s.ParentInventory == this && - ((s.SlotIndex < 0 || s.SlotIndex >= Items.Length || Items[s.SlotIndex] == null) || (Character.Controlled != null && !Character.Controlled.CanAccessInventory(s.Inventory)))); + ((s.SlotIndex < 0 || s.SlotIndex >= slots.Length || slots[s.SlotIndex] == null) || (Character.Controlled != null && !Character.Controlled.CanAccessInventory(s.Inventory)))); foreach (var highlightedSubInventorySlot in highlightedSubInventorySlots) { if (highlightedSubInventorySlot.ParentInventory == this) @@ -558,7 +558,7 @@ namespace Barotrauma if (!highlightedSubInventorySlot.Inventory.IsInventoryHoverAvailable(character, null)) continue; Rectangle hoverArea = GetSubInventoryHoverArea(highlightedSubInventorySlot); - if (highlightedSubInventorySlot.Inventory?.slots == null || (!hoverArea.Contains(PlayerInput.MousePosition))) + if (highlightedSubInventorySlot.Inventory?.visualSlots == null || (!hoverArea.Contains(PlayerInput.MousePosition))) { hideSubInventories.Add(highlightedSubInventorySlot); } @@ -585,19 +585,19 @@ namespace Barotrauma // In sub editor we cannot hover over the slot because they are not rendered so we override it here if (Screen.Selected is SubEditorScreen subEditor && !subEditor.WiringMode) { - for (int i = 0; i < slots.Length; i++) + for (int i = 0; i < visualSlots.Length; i++) { var subInventory = GetSubInventory(i); if (subInventory != null) { - ShowSubInventory(new SlotReference(this, slots[i], i, false, Items[i].GetComponent().Inventory), deltaTime, cam, hideSubInventories, true); + ShowSubInventory(new SlotReference(this, visualSlots[i], i, false, subInventory), deltaTime, cam, hideSubInventories, true); } } } foreach (var subInventorySlot in hideSubInventories) { - if (subInventorySlot.Inventory == null) continue; + if (subInventorySlot.Inventory == null) { continue; } subInventorySlot.Inventory.HideTimer -= deltaTime; if (subInventorySlot.Inventory.HideTimer < 0.25f) { @@ -614,10 +614,10 @@ namespace Barotrauma for (int i = 0; i < capacity; i++) { - var item = Items[i]; + var item = slots[i].FirstOrDefault(); if (item != null) { - if (HideSlot(i)) continue; + if (HideSlot(i)) { continue; } if (character.HasEquippedItem(item)) // Keep a subinventory display open permanently when the container is equipped { var itemContainer = item.GetComponent(); @@ -626,24 +626,32 @@ namespace Barotrauma character.CanAccessInventory(itemContainer.Inventory) && !highlightedSubInventorySlots.Any(s => s.Inventory == itemContainer.Inventory)) { - ShowSubInventory(new SlotReference(this, slots[i], i, false, itemContainer.Inventory), deltaTime, cam, hideSubInventories, true); + ShowSubInventory(new SlotReference(this, visualSlots[i], i, false, itemContainer.Inventory), deltaTime, cam, hideSubInventories, true); } } } } } - if (doubleClickedItem != null) + if (doubleClickedItems.Any()) { - QuickUseItem(doubleClickedItem, true, true, true); + var quickUseAction = GetQuickUseAction(doubleClickedItems.First(), true, true, true); + foreach (Item doubleClickedItem in doubleClickedItems) + { + QuickUseItem(doubleClickedItem, true, true, true, quickUseAction, playSound: doubleClickedItem == doubleClickedItems.First()); + if (quickUseAction == QuickUseAction.Equip || quickUseAction == QuickUseAction.UseTreatment) + { + break; + } + } } for (int i = 0; i < capacity; i++) { - var item = Items[i]; + var item = slots[i].FirstOrDefault(); if (item != null) { - var slot = slots[i]; + var slot = visualSlots[i]; if (item.AllowedSlots.Any(a => a != InvSlotType.Any)) { HandleButtonEquipStates(item, slot, deltaTime); @@ -652,10 +660,10 @@ namespace Barotrauma } //cancel dragging if too far away from the container of the dragged item - if (draggingItem != null) + if (DraggingItems.Any()) { - var rootContainer = draggingItem.GetRootContainer(); - var rootInventory = draggingItem.ParentInventory; + var rootContainer = DraggingItems.First().GetRootContainer(); + var rootInventory = DraggingItems.First().ParentInventory; if (rootContainer != null) { @@ -673,27 +681,39 @@ namespace Barotrauma Character.Controlled.SelectedConstruction != null && rootContainer.linkedTo.Contains(Character.Controlled.SelectedConstruction))) { - draggingItem = null; + DraggingItems.Clear(); } } } - - doubleClickedItem = null; + doubleClickedItems.Clear(); } public void UpdateSlotInput() { for (int i = 0; i < capacity; i++) { - if (Items[i] != null && Items[i] != draggingItem && Character.Controlled?.Inventory == this && - GUI.KeyboardDispatcher.Subscriber == null && !CrewManager.IsCommandInterfaceOpen && PlayerInput.InventoryKeyHit(slots[i].InventoryKeyIndex)) + var firstItem = slots[i].FirstOrDefault(); + if (firstItem != null && !DraggingItems.Contains(firstItem) && Character.Controlled?.Inventory == this && + GUI.KeyboardDispatcher.Subscriber == null && !CrewManager.IsCommandInterfaceOpen && PlayerInput.InventoryKeyHit(visualSlots[i].InventoryKeyIndex)) { - QuickUseItem(Items[i], true, false, true); +#if LINUX + // some window managers on Linux use windows key + number to change workspaces or perform other actions + if (PlayerInput.KeyDown(Keys.RightWindows) || PlayerInput.KeyDown(Keys.LeftWindows)) { continue; } +#endif + var quickUseAction = GetQuickUseAction(firstItem, true, false, true); + foreach (Item itemToUse in slots[i].Items.ToList()) + { + QuickUseItem(itemToUse, true, true, true, quickUseAction, playSound: itemToUse == firstItem); + if (quickUseAction == QuickUseAction.Equip || quickUseAction == QuickUseAction.UseTreatment) + { + break; + } + } } } } - private void HandleButtonEquipStates(Item item, InventorySlot slot, float deltaTime) + private void HandleButtonEquipStates(Item item, VisualSlot slot, float deltaTime) { slot.EquipButtonState = slot.EquipButtonRect.Contains(PlayerInput.MousePosition) ? GUIComponent.ComponentState.Hover : GUIComponent.ComponentState.None; @@ -713,7 +733,7 @@ namespace Barotrauma if (quickUseAction != QuickUseAction.Drop) { slot.QuickUseButtonToolTip = quickUseAction == QuickUseAction.None ? - "" : TextManager.GetWithVariable("QuickUseAction." + quickUseAction.ToString(), "[equippeditem]", character.SelectedItems.FirstOrDefault(i => i != null)?.Name); + "" : TextManager.GetWithVariable("QuickUseAction." + quickUseAction.ToString(), "[equippeditem]", item?.Name); if (PlayerInput.PrimaryMouseButtonDown()) { slot.EquipButtonState = GUIComponent.ComponentState.Pressed; } if (PlayerInput.PrimaryMouseButtonClicked()) { @@ -726,8 +746,8 @@ namespace Barotrauma { for (int i = 0; i < indicators.Length; i++) { - if (indicatorIndexes[i] < 0) { continue; } - Item item = Items[indicatorIndexes[i]]; + if (indicatorIndices[i] < 0) { continue; } + Item item = slots[indicatorIndices[i]].FirstOrDefault(); if (item != null) { Wearable wearable = item.GetComponent(); @@ -795,12 +815,12 @@ namespace Barotrauma public void AssignQuickUseNumKeys() { int keyBindIndex = 0; - for (int i = 0; i < slots.Length; i++) + for (int i = 0; i < visualSlots.Length; i++) { if (HideSlot(i)) continue; if (SlotTypes[i] == InvSlotType.Any) { - slots[i].InventoryKeyIndex = keyBindIndex; + visualSlots[i].InventoryKeyIndex = keyBindIndex; keyBindIndex++; } } @@ -824,7 +844,7 @@ namespace Barotrauma { if (item.Container == null || character.Inventory.FindIndex(item.Container) == -1) // Not a subinventory in the character's inventory { - if (character.SelectedItems.Any(i => i?.OwnInventory != null && i.OwnInventory.CanBePut(item))) + if (character.HeldItems.Any(i => i.OwnInventory != null && i.OwnInventory.CanBePut(item))) { return QuickUseAction.PutToEquippedItem; } @@ -875,7 +895,7 @@ namespace Barotrauma { return QuickUseAction.TakeFromCharacter; } - else if (character.SelectedItems.Any(i => i?.OwnInventory != null && i.OwnInventory.CanBePut(item)) && allowInventorySwap) + else if (character.HeldItems.Any(i => i.OwnInventory != null && i.OwnInventory.CanBePut(item)) && allowInventorySwap) { return QuickUseAction.PutToEquippedItem; } @@ -901,23 +921,20 @@ namespace Barotrauma return QuickUseAction.None; } - private void QuickUseItem(Item item, bool allowEquip, bool allowInventorySwap, bool allowApplyTreatment) + private void QuickUseItem(Item item, bool allowEquip, bool allowInventorySwap, bool allowApplyTreatment, QuickUseAction? action = null, bool playSound = true) { if (Screen.Selected is SubEditorScreen editor && !editor.WiringMode && !Submarine.Unloading) { // Find the slot the item was contained in and flash it - if (item.ParentInventory?.slots != null) + if (item.ParentInventory?.visualSlots != null) { - var invSlots = item.ParentInventory.slots; - var invItems = item.ParentInventory.Items; + var invSlots = item.ParentInventory.visualSlots; for (int i = 0; i < invSlots.Length; i++) { - if (i < 0 || invSlots.Length <= i || i < 0 || invItems.Length <= i) { break; } + if (i < 0 || invSlots.Length <= i || i < 0 || item.ParentInventory.Capacity <= i) { break; } var slot = invSlots[i]; - var slotItem = invItems[i]; - - if (slotItem == item) + if (item.ParentInventory.GetItemAt(i) == item) { slot.ShowBorderHighlight(GUI.Style.Red, 0.1f, 0.4f); SoundPlayer.PlayUISound(GUISoundType.PickItem); @@ -931,8 +948,8 @@ namespace Barotrauma item.Remove(); return; } - - var quickUseAction = GetQuickUseAction(item, allowEquip, allowInventorySwap, allowApplyTreatment); + + QuickUseAction quickUseAction = action ?? GetQuickUseAction(item, allowEquip, allowInventorySwap, allowApplyTreatment); bool success = false; switch (quickUseAction) { @@ -963,7 +980,7 @@ namespace Barotrauma //attempt to put in a free slot first for (int i = capacity - 1; i >= 0; i--) { - if (Items[i] != null) { continue; } + if (!slots[i].Empty()) { continue; } if (SlotTypes[i] == InvSlotType.Any || !item.AllowedSlots.Any(a => a.HasFlag(SlotTypes[i]))) { continue; } success = TryPutItem(item, i, true, false, Character.Controlled, true); if (success) { break; } @@ -975,9 +992,10 @@ namespace Barotrauma { if (SlotTypes[i] == InvSlotType.Any || !item.AllowedSlots.Any(a => a.HasFlag(SlotTypes[i]))) { continue; } // something else already equipped in a hand slot, attempt to unequip it so items aren't unnecessarily swapped to it - if (Items[i] != null && Items[i].AllowedSlots.Contains(InvSlotType.Any) && (SlotTypes[i] == InvSlotType.LeftHand || SlotTypes[i] == InvSlotType.RightHand)) + if (!slots[i].Empty() && slots[i].First().AllowedSlots.Contains(InvSlotType.Any) && + (SlotTypes[i] == InvSlotType.LeftHand || SlotTypes[i] == InvSlotType.RightHand)) { - TryPutItem(Items[i], Character.Controlled, new List() { InvSlotType.Any }, true); + TryPutItem(slots[i].First(), Character.Controlled, new List() { InvSlotType.Any }, true); } success = TryPutItem(item, i, true, false, Character.Controlled, true); if (success) { break; } @@ -1040,15 +1058,15 @@ namespace Barotrauma } break; case QuickUseAction.PutToEquippedItem: - for (int i = 0; i < character.SelectedItems.Length; i++) + foreach (Item heldItem in character.HeldItems) { - if (character.SelectedItems[i]?.OwnInventory != null && - character.SelectedItems[i].OwnInventory.TryPutItem(item, Character.Controlled)) + if (heldItem.OwnInventory != null && + heldItem.OwnInventory.TryPutItem(item, Character.Controlled)) { success = true; for (int j = 0; j < capacity; j++) { - if (Items[j] == character.SelectedItems[i]) slots[j].ShowBorderHighlight(GUI.Style.Green, 0.1f, 0.4f); + if (slots[j].Contains(heldItem)) { visualSlots[j].ShowBorderHighlight(GUI.Style.Green, 0.1f, 0.4f); } } break; } @@ -1060,18 +1078,21 @@ namespace Barotrauma { for (int i = 0; i < capacity; i++) { - if (Items[i] == item) slots[i].ShowBorderHighlight(GUI.Style.Green, 0.1f, 0.4f); + if (slots[i].Contains(item)) { visualSlots[i].ShowBorderHighlight(GUI.Style.Green, 0.1f, 0.4f); } } } - draggingItem = null; - SoundPlayer.PlayUISound(success ? GUISoundType.PickItem : GUISoundType.PickItemFail); + DraggingItems.Clear(); + if (playSound) + { + SoundPlayer.PlayUISound(success ? GUISoundType.PickItem : GUISoundType.PickItemFail); + } } public void DrawOwn(SpriteBatch spriteBatch) { if (!AccessibleWhenAlive && !character.IsDead) { return; } - if (slots == null) { CreateSlots(); } + if (visualSlots == null) { CreateSlots(); } if (GameMain.GraphicsWidth != screenResolution.X || GameMain.GraphicsHeight != screenResolution.Y || prevUIScale != UIScale || @@ -1096,12 +1117,10 @@ namespace Barotrauma { if (HideSlot(i)) { continue; } - Rectangle interactRect = slots[i].InteractRect; - interactRect.Location += slots[i].DrawOffset.ToPoint(); - //don't draw the item if it's being dragged out of the slot - bool drawItem = draggingItem == null || draggingItem != Items[i] || interactRect.Contains(PlayerInput.MousePosition); - DrawSlot(spriteBatch, this, slots[i], Items[i], i, drawItem, SlotTypes[i]); + bool drawItem = !DraggingItems.Any() || !slots[i].Items.All(it => DraggingItems.Contains(it)) || visualSlots[i].MouseOn(); + + DrawSlot(spriteBatch, this, visualSlots[i], slots[i].FirstOrDefault(), i, drawItem, SlotTypes[i]); } if (hideButton != null && hideButton.Visible && !Locked) @@ -1109,48 +1128,48 @@ namespace Barotrauma hideButton.DrawManually(spriteBatch, alsoChildren: true); } - InventorySlot highlightedQuickUseSlot = null; + VisualSlot highlightedQuickUseSlot = null; Rectangle inventoryArea = Rectangle.Empty; for (int i = 0; i < capacity; i++) { if (HideSlot(i)) { continue; } - inventoryArea = inventoryArea == Rectangle.Empty ? slots[i].InteractRect : Rectangle.Union(inventoryArea, slots[i].InteractRect); + inventoryArea = inventoryArea == Rectangle.Empty ? visualSlots[i].InteractRect : Rectangle.Union(inventoryArea, visualSlots[i].InteractRect); - if (Items[i] == null || - (draggingItem == Items[i] && !slots[i].InteractRect.Contains(PlayerInput.MousePosition)) || - !Items[i].AllowedSlots.Any(a => a != InvSlotType.Any)) + if (slots[i].Empty() || + (DraggingItems.Any(it => slots[i].Contains(it)) && !visualSlots[i].InteractRect.Contains(PlayerInput.MousePosition)) || + !slots[i].First().AllowedSlots.Any(a => a != InvSlotType.Any)) { //draw limb icons on empty slots if (LimbSlotIcons.ContainsKey(SlotTypes[i])) { var icon = LimbSlotIcons[SlotTypes[i]]; - icon.Draw(spriteBatch, slots[i].Rect.Center.ToVector2() + slots[i].DrawOffset, GUI.Style.EquipmentSlotIconColor, origin: icon.size / 2, scale: slots[i].Rect.Width / icon.size.X); + icon.Draw(spriteBatch, visualSlots[i].Rect.Center.ToVector2() + visualSlots[i].DrawOffset, GUI.Style.EquipmentSlotIconColor, origin: icon.size / 2, scale: visualSlots[i].Rect.Width / icon.size.X); } continue; } - if (draggingItem == Items[i] && !slots[i].IsHighlighted) { continue; } + if (DraggingItems.Any(it => slots[i].Contains(it)) && !visualSlots[i].IsHighlighted) { continue; } //draw hand icons if the item is equipped in a hand slot - if (IsInLimbSlot(Items[i], InvSlotType.LeftHand)) + if (IsInLimbSlot(slots[i].First(), InvSlotType.LeftHand)) { var icon = LimbSlotIcons[InvSlotType.LeftHand]; - icon.Draw(spriteBatch, new Vector2(slots[i].Rect.X, slots[i].Rect.Bottom) + slots[i].DrawOffset, Color.White * 0.6f, origin: new Vector2(icon.size.X * 0.35f, icon.size.Y * 0.75f), scale: slots[i].Rect.Width / icon.size.X * 0.7f); + icon.Draw(spriteBatch, new Vector2(visualSlots[i].Rect.X, visualSlots[i].Rect.Bottom) + visualSlots[i].DrawOffset, Color.White * 0.6f, origin: new Vector2(icon.size.X * 0.35f, icon.size.Y * 0.75f), scale: visualSlots[i].Rect.Width / icon.size.X * 0.7f); } - if (IsInLimbSlot(Items[i], InvSlotType.RightHand)) + if (IsInLimbSlot(slots[i].First(), InvSlotType.RightHand)) { var icon = LimbSlotIcons[InvSlotType.RightHand]; - icon.Draw(spriteBatch, new Vector2(slots[i].Rect.Right, slots[i].Rect.Bottom) + slots[i].DrawOffset, Color.White * 0.6f, origin: new Vector2(icon.size.X * 0.65f, icon.size.Y * 0.75f), scale: slots[i].Rect.Width / icon.size.X * 0.7f); + icon.Draw(spriteBatch, new Vector2(visualSlots[i].Rect.Right, visualSlots[i].Rect.Bottom) + visualSlots[i].DrawOffset, Color.White * 0.6f, origin: new Vector2(icon.size.X * 0.65f, icon.size.Y * 0.75f), scale: visualSlots[i].Rect.Width / icon.size.X * 0.7f); } - GUIComponent.ComponentState state = slots[i].EquipButtonState; + GUIComponent.ComponentState state = visualSlots[i].EquipButtonState; if (state == GUIComponent.ComponentState.Hover) { - highlightedQuickUseSlot = slots[i]; + highlightedQuickUseSlot = visualSlots[i]; } - if (!Items[i].AllowedSlots.Any(a => a == InvSlotType.Any)) + if (!slots[i].First().AllowedSlots.Any(a => a == InvSlotType.Any)) { continue; } @@ -1161,20 +1180,20 @@ namespace Barotrauma color *= 0.5f; } - if (character.HasEquippedItem(Items[i])) + if (character.HasEquippedItem(slots[i].First())) { switch (state) { case GUIComponent.ComponentState.None: - EquippedIndicator.Draw(spriteBatch, slots[i].EquipButtonRect.Center.ToVector2(), color, EquippedIndicator.Origin, 0, UIScale * IndicatorScaleAdjustment); + EquippedIndicator.Draw(spriteBatch, visualSlots[i].EquipButtonRect.Center.ToVector2(), color, EquippedIndicator.Origin, 0, UIScale * IndicatorScaleAdjustment); break; case GUIComponent.ComponentState.Hover: - EquippedHoverIndicator.Draw(spriteBatch, slots[i].EquipButtonRect.Center.ToVector2(), color, EquippedIndicator.Origin, 0, UIScale * IndicatorScaleAdjustment); + EquippedHoverIndicator.Draw(spriteBatch, visualSlots[i].EquipButtonRect.Center.ToVector2(), color, EquippedIndicator.Origin, 0, UIScale * IndicatorScaleAdjustment); break; case GUIComponent.ComponentState.Pressed: case GUIComponent.ComponentState.Selected: case GUIComponent.ComponentState.HoverSelected: - EquippedClickedIndicator.Draw(spriteBatch, slots[i].EquipButtonRect.Center.ToVector2(), color, EquippedIndicator.Origin, 0, UIScale * IndicatorScaleAdjustment); + EquippedClickedIndicator.Draw(spriteBatch, visualSlots[i].EquipButtonRect.Center.ToVector2(), color, EquippedIndicator.Origin, 0, UIScale * IndicatorScaleAdjustment); break; } } @@ -1183,15 +1202,15 @@ namespace Barotrauma switch (state) { case GUIComponent.ComponentState.None: - UnequippedIndicator.Draw(spriteBatch, slots[i].EquipButtonRect.Center.ToVector2(), color, EquippedIndicator.Origin, 0, UIScale * IndicatorScaleAdjustment); + UnequippedIndicator.Draw(spriteBatch, visualSlots[i].EquipButtonRect.Center.ToVector2(), color, EquippedIndicator.Origin, 0, UIScale * IndicatorScaleAdjustment); break; case GUIComponent.ComponentState.Hover: - UnequippedHoverIndicator.Draw(spriteBatch, slots[i].EquipButtonRect.Center.ToVector2(), color, EquippedIndicator.Origin, 0, UIScale * IndicatorScaleAdjustment); + UnequippedHoverIndicator.Draw(spriteBatch, visualSlots[i].EquipButtonRect.Center.ToVector2(), color, EquippedIndicator.Origin, 0, UIScale * IndicatorScaleAdjustment); break; case GUIComponent.ComponentState.Pressed: case GUIComponent.ComponentState.Selected: case GUIComponent.ComponentState.HoverSelected: - UnequippedClickedIndicator.Draw(spriteBatch, slots[i].EquipButtonRect.Center.ToVector2(), color, EquippedIndicator.Origin, 0, UIScale * IndicatorScaleAdjustment); + UnequippedClickedIndicator.Draw(spriteBatch, visualSlots[i].EquipButtonRect.Center.ToVector2(), color, EquippedIndicator.Origin, 0, UIScale * IndicatorScaleAdjustment); break; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs index 7fb2f8093..a223189a9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs @@ -12,16 +12,14 @@ namespace Barotrauma.Items.Components get { return item.Rect.Size.ToVector2(); } } - - public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) { - if (!IsActive || picker == null || !CanBeAttached(picker) || !picker.IsKeyDown(InputType.Aim) || picker != Character.Controlled) + if (!IsActive || picker == null || !CanBeAttached(picker) || !picker.IsKeyDown(InputType.Aim) || picker != Character.Controlled) { Drawable = false; - return; + return; } - + Vector2 gridPos = picker.Position; Vector2 roundedGridPos = new Vector2( MathUtils.RoundTowardsClosest(picker.Position.X, Submarine.GridSize.X), @@ -49,7 +47,7 @@ namespace Barotrauma.Items.Components attachPos += item.Submarine.Position; } - Submarine.DrawGrid(spriteBatch, 14, gridPos, roundedGridPos, alpha: 0.7f); + Submarine.DrawGrid(spriteBatch, 14, gridPos, roundedGridPos, alpha: 0.4f); item.Sprite.Draw( spriteBatch, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs index 7432e038a..c9c4fcb7a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs @@ -130,7 +130,7 @@ namespace Barotrauma.Items.Components if (body.UserData is Item item) { var door = item.GetComponent(); - if (door != null && door.IsOpen || door.IsBroken) continue; + if (door != null && door.CanBeTraversed) { continue; } } targetHull = null; @@ -239,7 +239,7 @@ namespace Barotrauma.Items.Components { if (targetSections.Count == 0) { return; } - Item liquidItem = liquidContainer?.Inventory.Items[0]; + Item liquidItem = liquidContainer?.Inventory.FirstOrDefault(); if (liquidItem == null) { return; } bool isCleaning = false; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index 8dd46bcd2..f99a471ca 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -15,7 +15,8 @@ namespace Barotrauma.Items.Components Random, CharacterSpecific, ItemSpecific, - All + All, + Manual } class ItemSound @@ -259,6 +260,7 @@ namespace Barotrauma.Items.Components loopingSoundChannel = null; loopingSound = null; } + if (loopingSoundChannel == null || !loopingSoundChannel.IsPlaying) { loopingSoundChannel = loopingSound.RoundSound.Sound.Play( @@ -271,6 +273,21 @@ namespace Barotrauma.Items.Components loopingSoundChannel.Near = loopingSound.Range * 0.4f; loopingSoundChannel.Far = loopingSound.Range; } + + // Looping sound with manual selection mode should be changed if value of ManuallySelectedSound has changed + // Otherwise the sound won't change until the sound condition (such as being active) is disabled and re-enabled + if (loopingSoundChannel != null && loopingSoundChannel.IsPlaying && soundSelectionModes[type] == SoundSelectionMode.Manual) + { + var playingIndex = sounds[type].IndexOf(loopingSound); + var shouldBePlayingIndex = Math.Clamp(ManuallySelectedSound, 0, sounds[type].Count); + if (playingIndex != shouldBePlayingIndex) + { + loopingSoundChannel.FadeOutAndDispose(); + loopingSoundChannel = null; + loopingSound = null; + } + } + return; } @@ -295,6 +312,10 @@ namespace Barotrauma.Items.Components } return; } + else if (soundSelectionMode == SoundSelectionMode.Manual) + { + index = Math.Clamp(ManuallySelectedSound, 0, matchingSounds.Count); + } else { index = Rand.Int(matchingSounds.Count); @@ -335,7 +356,7 @@ namespace Barotrauma.Items.Components { float volume = GetSoundVolume(itemSound); if (volume <= 0.0001f) { return; } - var channel = SoundPlayer.PlaySound(itemSound.RoundSound.Sound, position, volume, itemSound.Range, itemSound.RoundSound.GetRandomFrequencyMultiplier(), item.CurrentHull); + var channel = SoundPlayer.PlaySound(itemSound.RoundSound.Sound, position, volume, itemSound.Range, itemSound.RoundSound.GetRandomFrequencyMultiplier(), item.CurrentHull, ignoreMuffling: itemSound.RoundSound.IgnoreMuffling); if (channel != null) { playingOneshotSoundChannels.Add(channel); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs index abef03494..32abbb2a1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Xml.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; @@ -32,6 +33,12 @@ namespace Barotrauma.Items.Components private set; } + public Sprite ContainedStateIndicatorEmpty + { + get; + private set; + } + #if DEBUG [Editable] #endif @@ -55,6 +62,11 @@ namespace Barotrauma.Items.Components [Serialize(null, false, description: "An optional text displayed above the item's inventory.")] public string UILabel { get; set; } + public GUIComponentStyle IndicatorStyle { get; set; } + + [Serialize(null, false)] + public string ContainedStateIndicatorStyle { get; set; } + [Serialize(true, false, description: "Should an indicator displaying the state of the contained items be displayed on this item's inventory slot. "+ "If this item can only contain one item, the indicator will display the condition of the contained item, otherwise it will indicate how full the item is.")] public bool ShowContainedStateIndicator { get; set; } @@ -98,6 +110,26 @@ namespace Barotrauma.Items.Components case "containedstateindicator": ContainedStateIndicator = new Sprite(subElement); break; + case "containedstateindicatorempty": + ContainedStateIndicatorEmpty = new Sprite(subElement); + break; + } + } + + if (string.IsNullOrEmpty(ContainedStateIndicatorStyle)) + { + //if neither a style or a custom sprite is defined, use default style + if (ContainedStateIndicator == null) + { + IndicatorStyle = GUI.Style.GetComponentStyle("ContainedStateIndicator.Default"); + } + } + else + { + IndicatorStyle = GUI.Style.GetComponentStyle("ContainedStateIndicator." + ContainedStateIndicatorStyle); + if (ContainedStateIndicator != null || ContainedStateIndicatorEmpty != null) + { + DebugConsole.AddWarning($"Item \"{item.Name}\" defines both a contained state indicator style and a custom indicator sprite. Will use the custom sprite..."); } } if (GuiFrame == null) @@ -173,15 +205,9 @@ namespace Barotrauma.Items.Components } //if holding 2 different "always open" items in different hands, don't force them to stay open - if (character.SelectedItems[0] != null && - character.SelectedItems[1] != null && - character.SelectedItems[0] != character.SelectedItems[1]) + if (character.HeldItems.Count() > 1 && character.HeldItems.All(it => it.GetComponent()?.KeepOpenWhenEquipped ?? false)) { - if ((character.SelectedItems[0].GetComponent()?.KeepOpenWhenEquipped ?? false) && - (character.SelectedItems[1].GetComponent()?.KeepOpenWhenEquipped ?? false)) - { - return false; - } + return false; } return true; @@ -260,10 +286,8 @@ namespace Barotrauma.Items.Components bool isWiringMode = SubEditorScreen.TransparentWiringMode && SubEditorScreen.IsWiringMode(); int i = 0; - foreach (Item containedItem in Inventory.Items) + foreach (Item containedItem in Inventory.AllItems) { - if (containedItem == null) continue; - if (AutoInteractWithContained) { containedItem.IsHighlighted = item.IsHighlighted; @@ -313,7 +337,7 @@ namespace Barotrauma.Items.Components public override void UpdateHUD(Character character, float deltaTime, Camera cam) { - if (item.NonInteractable) { return; } + if (!item.IsInteractable(character)) { return; } if (Inventory.RectTransform != null) { guiCustomComponent.RectTransform.Parent = Inventory.RectTransform; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs index 2dd59dedd..dd606cad2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs @@ -21,11 +21,17 @@ namespace Barotrauma.Items.Components private float[] charWidths; + private Vector4 padding; + [Serialize("0,0,0,0", true, description: "The amount of padding around the text in pixels (left,top,right,bottom).")] public Vector4 Padding { - get { return TextBlock.Padding; } - set { TextBlock.Padding = value; } + get { return padding; } + set + { + padding = value; + TextBlock.Padding = value * item.Scale; + } } private string text; @@ -41,15 +47,22 @@ namespace Barotrauma.Items.Components { textBlock = null; } - + text = value; - DisplayText = TextManager.Get(text, returnNull: true) ?? value; - TextBlock.Text = DisplayText; - if (Screen.Selected == GameMain.SubEditorScreen && Scrollable) - { - TextBlock.Text = ToolBox.LimitString(DisplayText, textBlock.Font, item.Rect.Width); - } - SetScrollingText(); + SetDisplayText(value); + } + } + + private bool ignoreLocalization; + + [Editable, Serialize(false, true, "Whether or not to skip localization and always display the raw value.")] + public bool IgnoreLocalization + { + get => ignoreLocalization; + set + { + ignoreLocalization = value; + SetDisplayText(Text); } } @@ -107,13 +120,7 @@ namespace Barotrauma.Items.Components { if (textBlock == null) { - textBlock = new GUITextBlock(new RectTransform(item.Rect.Size), "", - textColor: textColor, font: GUI.UnscaledSmallFont, textAlignment: scrollable ? Alignment.CenterLeft : Alignment.Center, wrap: true, style: null) - { - TextDepth = item.SpriteDepth - 0.00001f, - RoundToNearestPixel = false, - TextScale = TextScale - }; + RecreateTextBlock(); } return textBlock; } @@ -126,7 +133,7 @@ namespace Barotrauma.Items.Components private void SetScrollingText() { - if (!scrollable) return; + if (!scrollable) { return; } float totalWidth = textBlock.Font.MeasureString(DisplayText).X; float textAreaWidth = Math.Max(textBlock.Rect.Width - textBlock.Padding.X - textBlock.Padding.Z, 0); @@ -143,6 +150,7 @@ namespace Barotrauma.Items.Components //whole text can fit in the textblock, no need to scroll needsScrolling = false; scrollingText = DisplayText; + scrollPadding = 0; scrollAmount = 0.0f; scrollIndex = 0; return; @@ -161,16 +169,40 @@ namespace Barotrauma.Items.Components scrollIndex = MathHelper.Clamp(scrollIndex, 0, DisplayText.Length); } + private void SetDisplayText(string value) + { + DisplayText = IgnoreLocalization ? value : TextManager.Get(value, returnNull: true) ?? value; + TextBlock.Text = DisplayText; + if (Screen.Selected == GameMain.SubEditorScreen && Scrollable) + { + TextBlock.Text = ToolBox.LimitString(DisplayText, TextBlock.Font, item.Rect.Width); + } + + SetScrollingText(); + } + + private void RecreateTextBlock() + { + textBlock = new GUITextBlock(new RectTransform(item.Rect.Size), "", + textColor: textColor, font: GUI.UnscaledSmallFont, textAlignment: scrollable ? Alignment.CenterLeft : Alignment.Center, wrap: !scrollable, style: null) + { + TextDepth = item.SpriteDepth - 0.00001f, + RoundToNearestPixel = false, + TextScale = TextScale, + Padding = padding * item.Scale + }; + } + public override void Update(float deltaTime, Camera cam) { - if (!scrollable) return; + if (!scrollable) { return; } if (scrollingText == null) { SetScrollingText(); } - if (!needsScrolling) return; + if (!needsScrolling) { return; } scrollAmount -= deltaTime * ScrollSpeed; @@ -204,11 +236,33 @@ namespace Barotrauma.Items.Components } } - TextBlock.Text = sb.ToString(); + TextBlock.Text = sb.ToString(); } - + + public override void OnScaleChanged() + { + RecreateTextBlock(); + SetDisplayText(Text); + prevScale = item.Scale; + prevRect = item.Rect; + } + + private float prevScale; + private Rectangle prevRect; + public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) { + if (editing) + { + if (!MathUtils.NearlyEqual(prevScale, item.Scale) || prevRect != item.Rect) + { + RecreateTextBlock(); + SetDisplayText(Text); + prevScale = item.Scale; + prevRect = item.Rect; + } + } + var drawPos = new Vector2( item.DrawPosition.X - item.Rect.Width / 2.0f, -(item.DrawPosition.Y + item.Rect.Height / 2.0f)); @@ -223,7 +277,7 @@ namespace Barotrauma.Items.Components } textBlock.TextDepth = item.SpriteDepth - 0.0001f; - textBlock.TextOffset = drawPos - textBlock.Rect.Location.ToVector2() + new Vector2(scrollAmount + scrollPadding, 0.0f); + textBlock.TextOffset = drawPos - textBlock.Rect.Location.ToVector2() + (editing ? Vector2.Zero : new Vector2(scrollAmount + scrollPadding, 0.0f)); textBlock.DrawManually(spriteBatch); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs index 7c761645a..b3a228c25 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs @@ -38,12 +38,6 @@ namespace Barotrauma.Items.Components light.Color = LightColor.Multiply(brightness); } - public override void OnItemLoaded() - { - base.OnItemLoaded(); - SetLightSourceState(IsActive, lightBrightness); - } - public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) { if (light.LightSprite != null && (item.body == null || item.body.Enabled) && lightBrightness > 0.0f && IsOn) @@ -51,7 +45,7 @@ namespace Barotrauma.Items.Components Vector2 origin = light.LightSprite.Origin; if ((light.LightSpriteEffect & SpriteEffects.FlipHorizontally) == SpriteEffects.FlipHorizontally) { origin.X = light.LightSprite.SourceRect.Width - origin.X; } if ((light.LightSpriteEffect & SpriteEffects.FlipVertically) == SpriteEffects.FlipVertically) { origin.Y = light.LightSprite.SourceRect.Height - origin.Y; } - light.LightSprite.Draw(spriteBatch, new Vector2(item.DrawPosition.X, -item.DrawPosition.Y), lightColor * lightBrightness, origin, -light.Rotation, item.Scale, light.LightSpriteEffect, item.SpriteDepth - 0.0001f); + light.LightSprite.Draw(spriteBatch, new Vector2(item.DrawPosition.X, -item.DrawPosition.Y), lightColor * lightBrightness, origin, -light.Rotation, item.Scale, light.LightSpriteEffect, itemDepth - 0.0001f); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs index 2f1cc9cad..711376751 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs @@ -95,11 +95,11 @@ namespace Barotrauma.Items.Components // TODO, This works fine as of now but if GUI.PreventElementOverlap ever gets fixed this block of code may become obsolete or detrimental. // Only do this if there's only one linked component. If you link more containers then may // GUI.PreventElementOverlap have mercy on your HUD layout - if (GuiFrame != null && item.linkedTo.Count(entity => entity is Item item && item.DisplaySideBySideWhenLinked) == 1) + if (GuiFrame != null && item.linkedTo.Count(entity => entity is Item { DisplaySideBySideWhenLinked: true }) == 1) { foreach (MapEntity linkedTo in item.linkedTo) { - if (!(linkedTo is Item linkedItem) || !linkedItem.DisplaySideBySideWhenLinked) { continue; } + if (!(linkedTo is Item { DisplaySideBySideWhenLinked: true } linkedItem)) { continue; } if (!linkedItem.Components.Any()) { continue; } var itemContainer = linkedItem.GetComponent(); @@ -108,8 +108,8 @@ namespace Barotrauma.Items.Components // how much spacing do we want between the components var padding = (int) (8 * GUI.Scale); // Move the linked container to the right and move the deconstructor to the left - itemContainer.GuiFrame.RectTransform.AbsoluteOffset = new Point(100, 0); - GuiFrame.RectTransform.AbsoluteOffset = new Point(-100, 0); + itemContainer.GuiFrame.RectTransform.AbsoluteOffset = new Point(GuiFrame.Rect.Width / -2 - padding, 0); + GuiFrame.RectTransform.AbsoluteOffset = new Point(itemContainer.GuiFrame.Rect.Width / 2 + padding, 0); } } return base.Select(character); @@ -126,7 +126,7 @@ namespace Barotrauma.Items.Components private void DrawOverLay(SpriteBatch spriteBatch, GUICustomComponent overlayComponent) { overlayComponent.RectTransform.SetAsLastChild(); - var lastSlot = inputContainer.Inventory.slots.Last(); + var lastSlot = inputContainer.Inventory.visualSlots.Last(); GUI.DrawRectangle(spriteBatch, new Rectangle( diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs index 0ce476ce6..159e1c3f9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs @@ -144,16 +144,15 @@ namespace Barotrauma.Items.Components Vector2 drawPos = item.DrawPosition; drawPos += PropellerPos; drawPos.Y = -drawPos.Y; - propellerSprite.Draw(spriteBatch, (int)Math.Floor(spriteIndex), drawPos, Color.White, propellerSprite.Origin, 0.0f, Vector2.One); } - if (editing && !GUI.DisableHUD) + if (editing && !DisablePropellerDamage && propellerDamage != null && !GUI.DisableHUD) { Vector2 drawPos = item.DrawPosition; - drawPos += PropellerPos; + drawPos += PropellerPos * item.Scale; drawPos.Y = -drawPos.Y; - GUI.DrawRectangle(spriteBatch, drawPos - Vector2.One * 10, Vector2.One * 20, GUI.Style.Red); + spriteBatch.DrawCircle(drawPos, propellerDamage.DamageRange * item.Scale, 16, GUI.Style.Red, thickness: 2); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index 7adcb9eb3..0e1db1b6d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -190,7 +190,7 @@ namespace Barotrauma.Items.Components }; } - new GUITextBlock(new RectTransform(new Vector2(0.85f, 1f), container.RectTransform), fi.DisplayName) + new GUITextBlock(new RectTransform(new Vector2(0.85f, 1f), container.RectTransform), GetRecipeNameAndAmount(fi)) { Padding = Vector4.Zero, AutoScaleVertical = true, @@ -199,6 +199,21 @@ namespace Barotrauma.Items.Components } } + private string GetRecipeNameAndAmount(FabricationRecipe fabricationRecipe) + { + if (fabricationRecipe == null) { return ""; } + if (fabricationRecipe.Amount > 1) + { + return TextManager.GetWithVariables("fabricationrecipenamewithamount", + new string[2] { "[name]", "[amount]" }, + new string[2] { fabricationRecipe.DisplayName, fabricationRecipe.Amount.ToString() }); + } + else + { + return fabricationRecipe.DisplayName; + } + } + partial void OnItemLoadedProjSpecific() { inputContainer.AllowUIOverlap = true; @@ -212,11 +227,11 @@ namespace Barotrauma.Items.Components // TODO, This works fine as of now but if GUI.PreventElementOverlap ever gets fixed this block of code may become obsolete or detrimental. // Only do this if there's only one linked component. If you link more containers then may // GUI.PreventElementOverlap have mercy on your HUD layout - if (GuiFrame != null && item.linkedTo.Count(entity => entity is Item item && item.DisplaySideBySideWhenLinked) == 1) + if (GuiFrame != null && item.linkedTo.Count(entity => entity is Item { DisplaySideBySideWhenLinked: true }) == 1) { foreach (MapEntity linkedTo in item.linkedTo) { - if (!(linkedTo is Item linkedItem) || !linkedItem.DisplaySideBySideWhenLinked) { continue; } + if (!(linkedTo is Item { DisplaySideBySideWhenLinked: true } linkedItem)) { continue; } if (!linkedItem.Components.Any()) { continue; } var itemContainer = linkedItem.GetComponent(); @@ -225,8 +240,8 @@ namespace Barotrauma.Items.Components // how much spacing do we want between the components var padding = (int) (8 * GUI.Scale); // Move the linked container to the right and move the fabricator to the left - itemContainer.GuiFrame.RectTransform.AbsoluteOffset = new Point(-100, 0); - GuiFrame.RectTransform.AbsoluteOffset = new Point(100, 0); + itemContainer.GuiFrame.RectTransform.AbsoluteOffset = new Point(GuiFrame.Rect.Width / -2 - padding, 0); + GuiFrame.RectTransform.AbsoluteOffset = new Point(itemContainer.GuiFrame.Rect.Width / 2 + padding, 0); } } @@ -287,9 +302,8 @@ namespace Barotrauma.Items.Components missingItems.Add(requiredItem); } } - foreach (Item item in inputContainer.Inventory.Items) + foreach (Item item in inputContainer.Inventory.AllItems) { - if (item == null) { continue; } missingItems.Remove(missingItems.FirstOrDefault(mi => mi.ItemPrefabs.Contains(item.prefab))); } @@ -297,7 +311,7 @@ namespace Barotrauma.Items.Components foreach (FabricationRecipe.RequiredItem requiredItem in missingItems) { - while (slotIndex < inputContainer.Capacity && inputContainer.Inventory.Items[slotIndex] != null) + while (slotIndex < inputContainer.Capacity && inputContainer.Inventory.GetItemAt(slotIndex) != null) { slotIndex++; } @@ -307,17 +321,17 @@ namespace Barotrauma.Items.Components { if (item.ParentInventory != inputContainer.Inventory && IsItemValidIngredient(item, requiredItem)) { - int availableSlotIndex = Array.IndexOf(item.ParentInventory.Items, item); + int availableSlotIndex = item.ParentInventory.FindIndex(item); //slots are null if the inventory has never been displayed //(linked item, but the UI is not set to be displayed at the same time) - if (item.ParentInventory.slots != null) + if (item.ParentInventory.visualSlots != null) { - if (item.ParentInventory.slots[availableSlotIndex].HighlightTimer <= 0.0f) + if (item.ParentInventory.visualSlots[availableSlotIndex].HighlightTimer <= 0.0f) { - item.ParentInventory.slots[availableSlotIndex].ShowBorderHighlight(GUI.Style.Green, 0.5f, 0.5f, 0.2f); + item.ParentInventory.visualSlots[availableSlotIndex].ShowBorderHighlight(GUI.Style.Green, 0.5f, 0.5f, 0.2f); if (slotIndex < inputContainer.Capacity) { - inputContainer.Inventory.slots[slotIndex].ShowBorderHighlight(GUI.Style.Green, 0.5f, 0.5f, 0.2f); + inputContainer.Inventory.visualSlots[slotIndex].ShowBorderHighlight(GUI.Style.Green, 0.5f, 0.5f, 0.2f); } } } @@ -327,7 +341,7 @@ namespace Barotrauma.Items.Components if (slotIndex >= inputContainer.Capacity) { break; } var itemIcon = requiredItem.ItemPrefabs.First().InventoryIcon ?? requiredItem.ItemPrefabs.First().sprite; - Rectangle slotRect = inputContainer.Inventory.slots[slotIndex].Rect; + Rectangle slotRect = inputContainer.Inventory.visualSlots[slotIndex].Rect; itemIcon.Draw( spriteBatch, slotRect.Center.ToVector2(), @@ -367,14 +381,12 @@ namespace Barotrauma.Items.Components { overlayComponent.RectTransform.SetAsLastChild(); - if (outputContainer.Inventory.Items.First() != null) { return; } - FabricationRecipe targetItem = fabricatedItem ?? selectedItem; if (targetItem != null) { var itemIcon = targetItem.TargetItem.InventoryIcon ?? targetItem.TargetItem.sprite; - Rectangle slotRect = outputContainer.Inventory.slots[0].Rect; + Rectangle slotRect = outputContainer.Inventory.visualSlots[0].Rect; if (fabricatedItem != null) { @@ -449,8 +461,10 @@ namespace Barotrauma.Items.Components Color = selectedItem.TargetItem.InventoryIconColor }; }*/ + + var nameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform), - selectedItem.TargetItem.Name, textAlignment: Alignment.CenterLeft, textColor: Color.Aqua, font: GUI.SubHeadingFont) + GetRecipeNameAndAmount(selectedItem), textAlignment: Alignment.CenterLeft, textColor: Color.Aqua, font: GUI.SubHeadingFont) { AutoScaleHorizontal = true }; @@ -539,7 +553,7 @@ namespace Barotrauma.Items.Components private bool StartButtonClicked(GUIButton button, object obj) { if (selectedItem == null) { return false; } - if (!outputContainer.Inventory.IsEmpty()) + if (fabricatedItem == null && !outputContainer.Inventory.CanBePut(selectedItem.TargetItem)) { outputSlot.Flash(GUI.Style.Red); return false; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index 16fc3d2cc..19056309a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -71,10 +71,14 @@ namespace Barotrauma.Items.Components private float showDirectionalIndicatorTimer; - private List nearbyObjects = new List(); + private readonly List nearbyObjects = new List(); private const float NearbyObjectUpdateInterval = 1.0f; float nearbyObjectUpdateTimer; + private List connectedSubs = new List(); + private const float ConnectedSubUpdateInterval = 1.0f; + float connectedSubUpdateTimer; + //Vector2 = vector from the ping source to the position of the disruption //float = strength of the disruption, between 0-1 private readonly List> disruptedDirections = new List>(); @@ -135,6 +139,8 @@ namespace Barotrauma.Items.Components private bool isConnectedToSteering; + private static string caveLabel; + private bool AllowUsingMineralScanner => HasMineralScanner && !isConnectedToSteering; @@ -143,6 +149,10 @@ namespace Barotrauma.Items.Components System.Diagnostics.Debug.Assert(Enum.GetValues(typeof(BlipType)).Cast().All(t => blipColorGradient.ContainsKey(t))); sonarBlips = new List(); + caveLabel = + TextManager.Get("cave", returnNull: true) ?? + TextManager.Get("missiontype.nest"); + foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -413,6 +423,26 @@ namespace Barotrauma.Items.Components networkUpdateTimer -= deltaTime; } + connectedSubUpdateTimer -= deltaTime; + if (connectedSubUpdateTimer <= 0.0f) + { + connectedSubs.Clear(); + if (UseTransducers) + { + foreach (var transducer in connectedTransducers) + { + if (transducer.Transducer.Item.Submarine == null) { continue; } + if (connectedSubs.Contains(transducer.Transducer.Item.Submarine)) { continue; } + connectedSubs = transducer.Transducer.Item.Submarine?.GetConnectedSubs(); + } + } + else if (item.Submarine != null) + { + connectedSubs = item.Submarine?.GetConnectedSubs(); + } + connectedSubUpdateTimer = ConnectedSubUpdateInterval; + } + if (sonarView.Rect.Contains(PlayerInput.MousePosition)) { float scrollSpeed = PlayerInput.ScrollWheelSpeed / 1000.0f; @@ -848,10 +878,22 @@ namespace Barotrauma.Items.Components displayScale, center, DisplayRadius); } + for (int i = 0; i < Level.Loaded.Caves.Count; i++) + { + var cave = Level.Loaded.Caves[i]; + if (!cave.DisplayOnSonar) { continue; } + DrawMarker(spriteBatch, + caveLabel, + "cave", + "cave" + i, + cave.StartPos.ToVector2(), transducerCenter, + displayScale, center, DisplayRadius); + } + foreach (AITarget aiTarget in AITarget.List) { - if (!aiTarget.Enabled) continue; - if (string.IsNullOrEmpty(aiTarget.SonarLabel) || aiTarget.SoundRange <= 0.0f) continue; + if (!aiTarget.Enabled) { continue; } + if (string.IsNullOrEmpty(aiTarget.SonarLabel) || aiTarget.SoundRange <= 0.0f) { continue; } if (Vector2.DistanceSquared(aiTarget.WorldPosition, transducerCenter) < aiTarget.SoundRange * aiTarget.SoundRange) { @@ -902,16 +944,11 @@ namespace Barotrauma.Items.Components foreach (Submarine sub in Submarine.Loaded) { if (!sub.ShowSonarMarker) { continue; } - if (UseTransducers ? - connectedTransducers.Any(t => sub == t.Transducer.Item.Submarine || sub.DockedTo.Contains(t.Transducer.Item.Submarine)) : - (sub == item.Submarine || sub.DockedTo.Contains(item.Submarine))) - { - continue; - } + if (connectedSubs.Contains(sub)) { continue; } if (sub.WorldPosition.Y > Level.Loaded.Size.Y) { continue; } - - DrawMarker(spriteBatch, - sub.Info.DisplayName, + + DrawMarker(spriteBatch, + sub.Info.DisplayName, sub.Info.HasTag(SubmarineTag.Shuttle) ? "shuttle" : "submarine", sub, sub.WorldPosition, transducerCenter, @@ -931,10 +968,8 @@ namespace Barotrauma.Items.Components foreach (Submarine submarine in Submarine.Loaded) { - if (UseTransducers ? - !connectedTransducers.Any(t => submarine == t.Transducer.Item.Submarine || submarine.DockedTo.Contains(t.Transducer.Item.Submarine)) : - submarine != item.Submarine && !submarine.DockedTo.Contains(item.Submarine)) continue; - if (submarine.HullVertices == null) continue; + if (!connectedSubs.Contains(submarine)) { continue; } + if (submarine.HullVertices == null) { continue; } Vector2 offset = ConvertUnits.ToSimUnits(submarine.WorldPosition - transducerCenter); @@ -1018,8 +1053,8 @@ namespace Barotrauma.Items.Components //don't show the docking ports of the opposing team on the sonar if (item.Submarine != null) { - if ((dockingPort.Item.Submarine.TeamID == Character.TeamType.Team1 && item.Submarine.TeamID == Character.TeamType.Team2) || - (dockingPort.Item.Submarine.TeamID == Character.TeamType.Team2 && item.Submarine.TeamID == Character.TeamType.Team1)) + if ((dockingPort.Item.Submarine.TeamID == CharacterTeamType.Team1 && item.Submarine.TeamID == CharacterTeamType.Team2) || + (dockingPort.Item.Submarine.TeamID == CharacterTeamType.Team2 && item.Submarine.TeamID == CharacterTeamType.Team1)) { continue; } @@ -1226,19 +1261,10 @@ namespace Barotrauma.Items.Components foreach (Submarine submarine in Submarine.Loaded) { - if (submarine.HullVertices == null) continue; + if (submarine.HullVertices == null) { continue; } if (!DetectSubmarineWalls) { - if (UseTransducers) - { - if (connectedTransducers.Any(t => submarine == t.Transducer.Item.Submarine || - submarine.DockedTo.Contains(t.Transducer.Item.Submarine))) continue; - } - else - { - if (item.Submarine == submarine) continue; - if (item.Submarine != null && item.Submarine.DockedTo.Contains(submarine)) continue; - } + if (connectedSubs.Contains(submarine)) { continue; } } for (int i = 0; i < submarine.HullVertices.Count; i++) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs index fa0aba619..622ea1cf9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs @@ -34,7 +34,7 @@ namespace Barotrauma.Items.Components private GUIComponent steerArea; - private GUITextBlock pressureWarningText; + private GUITextBlock pressureWarningText, iceSpireWarningText; private GUITextBlock tipContainer; @@ -311,6 +311,7 @@ namespace Barotrauma.Items.Components { Vector2 vel = controlledSub == null ? Vector2.Zero : controlledSub.Velocity; var realWorldVel = ConvertUnits.ToDisplayUnits(vel.X * Physics.DisplayToRealWorldRatio) * 3.6f; + if (controlledSub != null && controlledSub.FlippedX) { realWorldVel *= -1; } return ((int)realWorldVel).ToString(); }; break; @@ -440,8 +441,13 @@ namespace Barotrauma.Items.Components (spriteBatch, guiCustomComponent) => { DrawHUD(spriteBatch, guiCustomComponent.Rect); }, null); steerRadius = steerArea.Rect.Width / 2; - pressureWarningText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.25f), steerArea.RectTransform, Anchor.Center, Pivot.TopCenter), - TextManager.Get("SteeringDepthWarning"), Color.Red, GUI.LargeFont, Alignment.Center) + iceSpireWarningText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.25f), steerArea.RectTransform, Anchor.Center, Pivot.TopCenter), + TextManager.Get("NavTerminalIceSpireWarning"), GUI.Style.Red, GUI.SubHeadingFont, Alignment.Center, color: Color.Black * 0.8f, wrap: true) + { + Visible = false + }; + pressureWarningText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.25f), steerArea.RectTransform, Anchor.Center, Pivot.TopCenter), + TextManager.Get("SteeringDepthWarning"), GUI.Style.Red, GUI.SubHeadingFont, Alignment.Center, color: Color.Black * 0.8f) { Visible = false }; @@ -471,7 +477,11 @@ namespace Barotrauma.Items.Components public void AttachToSonarHUD(GUICustomComponent sonarView) { steerArea.Visible = false; - sonarView.OnDraw += (spriteBatch, guiCustomComponent) => { DrawHUD(spriteBatch, guiCustomComponent.Rect); }; + sonarView.OnDraw += (spriteBatch, guiCustomComponent) => + { + DrawHUD(spriteBatch, guiCustomComponent.Rect); + steerArea.DrawChildren(spriteBatch, recursive: true); + }; } public void DrawHUD(SpriteBatch spriteBatch, Rectangle rect) @@ -712,12 +722,13 @@ namespace Barotrauma.Items.Components } } - pressureWarningText.Visible = item.Submarine != null && item.Submarine.AtDamageDepth && Timing.TotalTime % 1.0f < 0.5f; + pressureWarningText.Visible = item.Submarine != null && item.Submarine.AtDamageDepth && Timing.TotalTime % 1.0f < 0.8f; + iceSpireWarningText.Visible = item.Submarine != null && !pressureWarningText.Visible && showIceSpireWarning && Timing.TotalTime % 1.0f < 0.8f; if (Vector2.DistanceSquared(PlayerInput.MousePosition, steerArea.Rect.Center.ToVector2()) < steerRadius * steerRadius) { if (PlayerInput.PrimaryMouseButtonHeld() && !CrewManager.IsCommandInterfaceOpen && !GameSession.IsTabMenuOpen && - (!GameMain.GameSession?.Campaign?.ShowCampaignUI ?? true) && !GUIMessageBox.MessageBoxes.Any()) + (!GameMain.GameSession?.Campaign?.ShowCampaignUI ?? true) && !GUIMessageBox.MessageBoxes.Any(msgBox => msgBox is GUIMessageBox { MessageBoxType: GUIMessageBox.Type.Default })) { Vector2 inputPos = PlayerInput.MousePosition - steerArea.Rect.Center.ToVector2(); inputPos.Y = -inputPos.Y; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs index 693ffa18e..a57f4abfa 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs @@ -113,25 +113,28 @@ namespace Barotrauma.Items.Components } } - partial void FixItemProjSpecific(Character user, float deltaTime, Item targetItem) + partial void FixItemProjSpecific(Character user, float deltaTime, Item targetItem, bool showProgressBar) { - float progressBarState = targetItem.ConditionPercentage / 100.0f; - if (!MathUtils.NearlyEqual(progressBarState, prevProgressBarState) || prevProgressBarTarget != targetItem) + if (showProgressBar) { - var door = targetItem.GetComponent(); - if (door == null || door.Stuck <= 0) + float progressBarState = targetItem.ConditionPercentage / 100.0f; + if (!MathUtils.NearlyEqual(progressBarState, prevProgressBarState) || prevProgressBarTarget != targetItem) { - Vector2 progressBarPos = targetItem.DrawPosition; - var progressBar = user?.UpdateHUDProgressBar( - targetItem, - progressBarPos, - progressBarState, - GUI.Style.Red, GUI.Style.Green, - progressBarState < prevProgressBarState ? "progressbar.cutting" : ""); - if (progressBar != null) { progressBar.Size = new Vector2(60.0f, 20.0f); } + var door = targetItem.GetComponent(); + if (door == null || door.Stuck <= 0) + { + Vector2 progressBarPos = targetItem.DrawPosition; + var progressBar = user?.UpdateHUDProgressBar( + targetItem, + progressBarPos, + progressBarState, + GUI.Style.Red, GUI.Style.Green, + progressBarState < prevProgressBarState ? "progressbar.cutting" : ""); + if (progressBar != null) { progressBar.Size = new Vector2(60.0f, 20.0f); } + } + prevProgressBarState = progressBarState; + prevProgressBarTarget = targetItem; } - prevProgressBarState = progressBarState; - prevProgressBarTarget = targetItem; } Vector2 particlePos = ConvertUnits.ToDisplayUnits(pickedPosition); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs index 72d42a906..18d7f5650 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs @@ -53,13 +53,9 @@ namespace Barotrauma.Items.Components { //if the Character using the panel has a wire item equipped //and the wire hasn't been connected yet, draw it on the panel - for (int i = 0; i < character.SelectedItems.Length; i++) + foreach (Item item in character.HeldItems) { - Item selectedItem = character.SelectedItems[i]; - - if (selectedItem == null) { continue; } - - Wire wireComponent = selectedItem.GetComponent(); + Wire wireComponent = item.GetComponent(); if (wireComponent != null) { equippedWire = wireComponent; @@ -94,7 +90,8 @@ namespace Barotrauma.Items.Components int linkIndex = c.FindWireIndex(DraggingConnected.Item); if (linkIndex > -1 || panel.DisconnectedWires.Contains(DraggingConnected)) { - Inventory.draggingItem = DraggingConnected.Item; + Inventory.DraggingItems.Clear(); + Inventory.DraggingItems.Add(DraggingConnected.Item); } } } @@ -182,7 +179,11 @@ namespace Barotrauma.Items.Components new Vector2(x + width / 2, y + height), null, panel, ""); - if (DraggingConnected == equippedWire) { Inventory.draggingItem = equippedWire.Item; } + if (DraggingConnected == equippedWire) + { + Inventory.DraggingItems.Clear(); + Inventory.DraggingItems.Add(equippedWire.Item); + } } } @@ -207,7 +208,7 @@ namespace Barotrauma.Items.Components //(so we don't drop the item when dropping the wire on a connection) if (mouseInRect || (GUI.MouseOn?.UserData is ConnectionPanel && GUI.MouseOn.MouseRect.Contains(PlayerInput.MousePosition))) { - Inventory.draggingItem = null; + Inventory.DraggingItems.Clear(); } } @@ -236,7 +237,7 @@ namespace Barotrauma.Items.Components { float connectorSpriteScale = (35.0f / connectionSprite.SourceRect.Width) * panel.Scale; - for (int i = 0; i < MaxLinked; i++) + for (int i = 0; i < MaxWires; i++) { if (wires[i] == null || wires[i].Hidden || (DraggingConnected == wires[i] && (mouseIn || Screen.Selected == GameMain.SubEditorScreen))) { continue; } if (wires[i].HiddenInGame && Screen.Selected == GameMain.GameScreen) { continue; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs index eb03f1032..2886e0e74 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ConnectionPanel.cs @@ -122,7 +122,7 @@ namespace Barotrauma.Items.Components msg.ReadUInt16(); //user ID foreach (Connection connection in Connections) { - for (int i = 0; i < Connection.MaxLinked; i++) + for (int i = 0; i < connection.MaxWires; i++) { msg.ReadUInt16(); } @@ -168,7 +168,7 @@ namespace Barotrauma.Items.Components foreach (Connection connection in Connections) { - for (int i = 0; i < Connection.MaxLinked; i++) + for (int i = 0; i < connection.MaxWires; i++) { ushort wireId = msg.ReadUInt16(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs index e872fc842..da095f186 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Items.Components { @@ -15,7 +14,7 @@ namespace Barotrauma.Items.Components private Point ElementMaxSize => new Point(uiElementContainer.Rect.Width, (int)(65 * GUI.yScale)); - partial void InitProjSpecific(XElement element) + partial void InitProjSpecific() { CreateGUI(); } @@ -37,41 +36,70 @@ namespace Barotrauma.Items.Components float elementSize = Math.Min(1.0f / visibleElements.Count(), 1); foreach (CustomInterfaceElement ciElement in visibleElements) { - if (!string.IsNullOrEmpty(ciElement.PropertyName)) + if (ciElement.HasPropertyName) { - var layoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, elementSize), uiElementContainer.RectTransform), isHorizontal: true) - { - RelativeSpacing = 0.02f, - UserData = ciElement - }; - new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), layoutGroup.RectTransform), - TextManager.Get(ciElement.Label, returnNull: true) ?? ciElement.Label); - var textBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), layoutGroup.RectTransform), "", style: "GUITextBoxNoIcon") + var layoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, elementSize), uiElementContainer.RectTransform), isHorizontal: true) { - OverflowClip = true, + RelativeSpacing = 0.02f, UserData = ciElement }; - //reset size restrictions set by the Style to make sure the elements can fit the interface - textBox.RectTransform.MinSize = textBox.Frame.RectTransform.MinSize = new Point(0, 0); - textBox.RectTransform.MaxSize = textBox.Frame.RectTransform.MaxSize = new Point(int.MaxValue, int.MaxValue); - textBox.OnDeselected += (tb, key) => + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), layoutGroup.RectTransform), + TextManager.Get(ciElement.Label, returnNull: true) ?? ciElement.Label); + if (!ciElement.IsIntegerInput) { - if (GameMain.Client == null) + var textBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), layoutGroup.RectTransform), ciElement.Signal, style: "GUITextBoxNoIcon") { - TextChanged(tb.UserData as CustomInterfaceElement, textBox.Text); - } - else + OverflowClip = true, + UserData = ciElement + }; + //reset size restrictions set by the Style to make sure the elements can fit the interface + textBox.RectTransform.MinSize = textBox.Frame.RectTransform.MinSize = new Point(0, 0); + textBox.RectTransform.MaxSize = textBox.Frame.RectTransform.MaxSize = new Point(int.MaxValue, int.MaxValue); + textBox.OnDeselected += (tb, key) => { - item.CreateClientEvent(this); - } - }; + if (GameMain.Client == null) + { + TextChanged(tb.UserData as CustomInterfaceElement, textBox.Text); + } + else + { + item.CreateClientEvent(this); + } + }; - textBox.OnEnterPressed += (tb, text) => + textBox.OnEnterPressed += (tb, text) => + { + tb.Deselect(); + return true; + }; + uiElements.Add(textBox); + } + else { - tb.Deselect(); - return true; - }; - uiElements.Add(textBox); + int.TryParse(ciElement.Signal, out int signal); + var numberInput = new GUINumberInput(new RectTransform(new Vector2(0.5f, 1.0f), layoutGroup.RectTransform), GUINumberInput.NumberType.Int) + { + UserData = ciElement, + MinValueInt = ciElement.NumberInputMin, + MaxValueInt = ciElement.NumberInputMax, + IntValue = Math.Clamp(signal, ciElement.NumberInputMin, ciElement.NumberInputMax) + }; + //reset size restrictions set by the Style to make sure the elements can fit the interface + numberInput.RectTransform.MinSize = numberInput.LayoutGroup.RectTransform.MinSize = new Point(0, 0); + numberInput.RectTransform.MaxSize = numberInput.LayoutGroup.RectTransform.MaxSize = new Point(int.MaxValue, int.MaxValue); + numberInput.OnValueChanged += (ni) => + { + if (GameMain.Client == null) + { + ValueChanged(ni.UserData as CustomInterfaceElement, ni.IntValue); + } + else + { + item.CreateClientEvent(this); + } + }; + uiElements.Add(numberInput); + } } else if (ciElement.ContinuousSignal) { @@ -175,7 +203,7 @@ namespace Barotrauma.Items.Components foreach (var uiElement in uiElements) { if (!(uiElement.UserData is CustomInterfaceElement element)) { continue; } - bool visible = Screen.Selected == GameMain.SubEditorScreen || element.StatusEffects.Any() || !string.IsNullOrEmpty(element.PropertyName) || (element.Connection != null && element.Connection.Wires.Any(w => w != null)); + bool visible = Screen.Selected == GameMain.SubEditorScreen || element.StatusEffects.Any() || element.HasPropertyName || (element.Connection != null && element.Connection.Wires.Any(w => w != null)); if (visible) { visibleElementCount++; } if (uiElement.Visible != visible) { @@ -203,36 +231,29 @@ namespace Barotrauma.Items.Components { if (uiElements[i] is GUIButton button) { - button.Text = string.IsNullOrWhiteSpace(customInterfaceElementList[i].Label) ? - TextManager.GetWithVariable("connection.signaloutx", "[num]", (i + 1).ToString()) : - customInterfaceElementList[i].Label; + button.Text = CreateLabelText(i); button.TextBlock.Wrap = button.Text.Contains(' '); } else if (uiElements[i] is GUITickBox tickBox) { - tickBox.Text = string.IsNullOrWhiteSpace(customInterfaceElementList[i].Label) ? - TextManager.GetWithVariable("connection.signaloutx", "[num]", (i + 1).ToString()) : - customInterfaceElementList[i].Label; + tickBox.Text = CreateLabelText(i); tickBox.TextBlock.Wrap = tickBox.Text.Contains(' '); } - if (uiElements[i] is GUITextBox textBox) + else if (uiElements[i] is GUITextBox || uiElements[i] is GUINumberInput) { - var textBlock = textBox.Parent.GetChild(); - textBlock.Text = string.IsNullOrWhiteSpace(customInterfaceElementList[i].Label) ? - TextManager.GetWithVariable("connection.signaloutx", "[num]", (i + 1).ToString()) : - customInterfaceElementList[i].Label; + var textBlock = uiElements[i].Parent.GetChild(); + textBlock.Text = CreateLabelText(i); textBlock.Wrap = textBlock.Text.Contains(' '); - - foreach (ISerializableEntity e in item.AllPropertyObjects) - { - if (e.SerializableProperties.ContainsKey(customInterfaceElementList[i].PropertyName)) - { - textBox.Text = e.SerializableProperties[customInterfaceElementList[i].PropertyName].GetValue(e) as string; - } - } } } + string CreateLabelText(int elementIndex) + { + return string.IsNullOrWhiteSpace(customInterfaceElementList[elementIndex].Label) ? + TextManager.GetWithVariable("connection.signaloutx", "[num]", (elementIndex + 1).ToString()) : + customInterfaceElementList[elementIndex].Label; + } + uiElementContainer.Recalculate(); var textBlocks = new List(); foreach (GUIComponent element in uiElementContainer.Children) @@ -258,14 +279,40 @@ namespace Barotrauma.Items.Components GUITextBlock.AutoScaleAndNormalize(textBlocks); } + partial void UpdateSignalsProjSpecific() + { + for (int i = 0; i < signals.Length && i < uiElements.Count; i++) + { + if (uiElements[i] is GUITextBox tb) + { + tb.Text = customInterfaceElementList[i].Signal; + } + else if (uiElements[i] is GUINumberInput ni) + { + if (ni.InputType == GUINumberInput.NumberType.Int) + { + int.TryParse(customInterfaceElementList[i].Signal, out int value); + ni.IntValue = value; + } + } + } + } + public void ClientWrite(IWriteMessage msg, object[] extraData = null) { //extradata contains an array of buttons clicked by the player (or nothing if the player didn't click anything) for (int i = 0; i < customInterfaceElementList.Count; i++) { - if (!string.IsNullOrEmpty(customInterfaceElementList[i].PropertyName)) + if (customInterfaceElementList[i].HasPropertyName) { - msg.Write(((GUITextBox)uiElements[i]).Text); + if (!customInterfaceElementList[i].IsIntegerInput) + { + msg.Write(((GUITextBox)uiElements[i]).Text); + } + else + { + msg.Write(((GUINumberInput)uiElements[i]).IntValue.ToString()); + } } else if (customInterfaceElementList[i].ContinuousSignal) { @@ -282,9 +329,17 @@ namespace Barotrauma.Items.Components { for (int i = 0; i < customInterfaceElementList.Count; i++) { - if (!string.IsNullOrEmpty(customInterfaceElementList[i].PropertyName)) + if (customInterfaceElementList[i].HasPropertyName) { - TextChanged(customInterfaceElementList[i], msg.ReadString()); + if (!customInterfaceElementList[i].IsIntegerInput) + { + TextChanged(customInterfaceElementList[i], msg.ReadString()); + } + else + { + int.TryParse(msg.ReadString(), out int value); + ValueChanged(customInterfaceElementList[i], value); + } } else { @@ -300,6 +355,8 @@ namespace Barotrauma.Items.Components } } } + + UpdateSignalsProjSpecific(); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs index 8cb27dcea..353e689fe 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs @@ -214,7 +214,10 @@ namespace Barotrauma.Items.Components roundedGridPos += item.Submarine.Position; } - Submarine.DrawGrid(spriteBatch, 14, gridPos, roundedGridPos, alpha: 0.7f); + if (!SubEditorScreen.IsSubEditor() || !SubEditorScreen.ShouldDrawGrid) + { + Submarine.DrawGrid(spriteBatch, 14, gridPos, roundedGridPos, alpha: 0.25f); + } WireSection.Draw( spriteBatch, this, @@ -286,10 +289,8 @@ namespace Barotrauma.Items.Components public static void UpdateEditing(List wires) { var doubleClicked = PlayerInput.DoubleClicked(); - - Wire equippedWire = - Character.Controlled?.SelectedItems[0]?.GetComponent() ?? - Character.Controlled?.SelectedItems[1]?.GetComponent(); + + Wire equippedWire = Character.Controlled.HeldItems.FirstOrDefault(it => it.GetComponent() != null)?.GetComponent(); if (equippedWire != null && GUI.MouseOn == null) { if (PlayerInput.PrimaryMouseButtonClicked() && Character.Controlled.SelectedConstruction == null) @@ -329,6 +330,9 @@ namespace Barotrauma.Items.Components nodeWorldPos = nodeWorldPos - sub.HiddenSubPosition - sub.Position; } + if (selectedNodeIndex.HasValue && selectedNodeIndex.Value >= draggingWire.nodes.Count) { selectedNodeIndex = null; } + if (highlightedNodeIndex.HasValue && highlightedNodeIndex.Value >= draggingWire.nodes.Count) { highlightedNodeIndex = null; } + if (selectedNodeIndex.HasValue) { if (!PlayerInput.IsShiftDown()) @@ -342,14 +346,15 @@ namespace Barotrauma.Items.Components } else { - if ((highlightedNodeIndex.HasValue && Vector2.DistanceSquared(nodeWorldPos, draggingWire.nodes[(int)highlightedNodeIndex]) > Submarine.GridSize.X * Submarine.GridSize.X) || + float dragDistance = Submarine.GridSize.X * Submarine.GridSize.Y; + dragDistance *= 0.5f; + if ((highlightedNodeIndex.HasValue && Vector2.DistanceSquared(nodeWorldPos, draggingWire.nodes[(int)highlightedNodeIndex]) >= dragDistance) || PlayerInput.IsShiftDown()) { selectedNodeIndex = highlightedNodeIndex; } } - MapEntity.SelectEntity(draggingWire.item); } @@ -396,6 +401,13 @@ namespace Barotrauma.Items.Components if (closestIndex > -1) { highlightedNodeIndex = closestIndex; + + Vector2 nudge = MapEntity.GetNudgeAmount(doHold: false); + if (nudge != Vector2.Zero && closestIndex < selectedWire.nodes.Count) + { + selectedWire.MoveNode(closestIndex, nudge); + } + //start dragging the node if (PlayerInput.PrimaryMouseButtonHeld()) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs index 47e50417d..0c09668cf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs @@ -148,20 +148,20 @@ namespace Barotrauma.Items.Components List texts = new List(); List textColors = new List(); - if (target.Info != null) - { - texts.Add(target.Name); - textColors.Add(GUI.Style.TextColor); - } + texts.Add(target.Info == null ? target.DisplayName : target.Info.DisplayName); + textColors.Add(GUI.Style.TextColor); if (target.IsDead) { texts.Add(TextManager.Get("Deceased")); textColors.Add(GUI.Style.Red); - texts.Add( - target.CauseOfDeath.Affliction?.CauseOfDeathDescription ?? - TextManager.AddPunctuation(':', TextManager.Get("CauseOfDeath"), TextManager.Get("CauseOfDeath." + target.CauseOfDeath.Type.ToString()))); - textColors.Add(GUI.Style.Red); + if (target.CauseOfDeath != null) + { + texts.Add( + target.CauseOfDeath.Affliction?.CauseOfDeathDescription ?? + TextManager.AddPunctuation(':', TextManager.Get("CauseOfDeath"), TextManager.Get("CauseOfDeath." + target.CauseOfDeath.Type.ToString()))); + textColors.Add(GUI.Style.Red); + } } else { @@ -170,6 +170,21 @@ namespace Barotrauma.Items.Components texts.Add(target.customInteractHUDText); textColors.Add(GUI.Style.Green); } + if (!target.IsIncapacitated && target.IsPet) + { + texts.Add(CharacterHUD.GetCachedHudText("PlayHint", GameMain.Config.KeyBindText(InputType.Use))); + textColors.Add(GUI.Style.Green); + } + if (target.CharacterHealth.UseHealthWindow && equipper?.FocusedCharacter == target && equipper.CanInteractWith(target, 160f, false)) + { + texts.Add(CharacterHUD.GetCachedHudText("HealHint", GameMain.Config.KeyBindText(InputType.Health))); + textColors.Add(GUI.Style.Green); + } + if (target.CanBeDragged) + { + texts.Add(CharacterHUD.GetCachedHudText("GrabHint", GameMain.Config.KeyBindText(InputType.Grab))); + textColors.Add(GUI.Style.Green); + } if (target.IsUnconscious) { @@ -181,7 +196,7 @@ namespace Barotrauma.Items.Components texts.Add(TextManager.Get("Stunned")); textColors.Add(GUI.Style.Orange); } - + int oxygenTextIndex = MathHelper.Clamp((int)Math.Floor((1.0f - (target.Oxygen / 100.0f)) * OxygenTexts.Length), 0, OxygenTexts.Length - 1); texts.Add(OxygenTexts[oxygenTextIndex]); textColors.Add(Color.Lerp(GUI.Style.Red, GUI.Style.Green, target.Oxygen / 100.0f)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs index 0f38b86d1..991bb560a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs @@ -181,14 +181,14 @@ namespace Barotrauma.Items.Components { if (moveSoundChannel == null && startMoveSound != null) { - moveSoundChannel = SoundPlayer.PlaySound(startMoveSound.Sound, item.WorldPosition, startMoveSound.Volume, startMoveSound.Range); + moveSoundChannel = SoundPlayer.PlaySound(startMoveSound.Sound, item.WorldPosition, startMoveSound.Volume, startMoveSound.Range, ignoreMuffling: startMoveSound.IgnoreMuffling); } else if (moveSoundChannel == null || !moveSoundChannel.IsPlaying) { if (moveSound != null) { moveSoundChannel.FadeOutAndDispose(); - moveSoundChannel = SoundPlayer.PlaySound(moveSound.Sound, item.WorldPosition, moveSound.Volume, moveSound.Range); + moveSoundChannel = SoundPlayer.PlaySound(moveSound.Sound, item.WorldPosition, moveSound.Volume, moveSound.Range, ignoreMuffling: moveSound.IgnoreMuffling); if (moveSoundChannel != null) moveSoundChannel.Looping = true; } } @@ -200,7 +200,7 @@ namespace Barotrauma.Items.Components if (endMoveSound != null && moveSoundChannel.Sound != endMoveSound.Sound) { moveSoundChannel.FadeOutAndDispose(); - moveSoundChannel = SoundPlayer.PlaySound(endMoveSound.Sound, item.WorldPosition, endMoveSound.Volume, endMoveSound.Range); + moveSoundChannel = SoundPlayer.PlaySound(endMoveSound.Sound, item.WorldPosition, endMoveSound.Volume, endMoveSound.Range, ignoreMuffling: endMoveSound.IgnoreMuffling); if (moveSoundChannel != null) moveSoundChannel.Looping = false; } else if (!moveSoundChannel.IsPlaying) @@ -286,40 +286,26 @@ namespace Barotrauma.Items.Components rotation + MathHelper.PiOver2, item.Scale, SpriteEffects.None, item.SpriteDepth + (barrelSprite.Depth - item.Sprite.Depth)); - if (!GameMain.DebugDraw && (!editing || GUI.DisableHUD || !item.IsSelected)) { return; } + if (!editing || GUI.DisableHUD || !item.IsSelected) { return; } const float widgetRadius = 60.0f; Vector2 center = new Vector2((float)Math.Cos((maxRotation + minRotation) / 2), (float)Math.Sin((maxRotation + minRotation) / 2)); GUI.DrawLine(spriteBatch, drawPos, - drawPos + new Vector2((float)Math.Cos((maxRotation + minRotation) / 2), (float)Math.Sin((maxRotation + minRotation) / 2)) * widgetRadius, + drawPos + center * widgetRadius, Color.LightGreen); - if (GameMain.DebugDraw) - { - center = new Vector2((float)Math.Cos(targetRotation), (float)Math.Sin(targetRotation)); - GUI.DrawLine(spriteBatch, - drawPos, - drawPos + center * widgetRadius, - Color.Red); - - for (int i = 0; i < 5; i++) - { - center = new Vector2((float)Math.Cos(rotation + (angularVelocity * 0.05f * i)), (float)Math.Sin(rotation + (angularVelocity * 0.05f * i))); - GUI.DrawLine(spriteBatch, - drawPos, - drawPos + center * widgetRadius, - Color.Lerp(Color.Black, Color.Yellow, i * 0.25f)); - } - } - const float coneRadius = 300.0f; float radians = maxRotation - minRotation; float circleRadius = coneRadius / Screen.Selected.Cam.Zoom * GUI.Scale; float lineThickness = 1f / Screen.Selected.Cam.Zoom; - if (radians > Math.PI * 2) + if (Math.Abs(minRotation - maxRotation) < 0.02f) + { + spriteBatch.DrawLine(drawPos, drawPos + center * circleRadius, GUI.Style.Green, thickness: lineThickness); + } + else if (radians > Math.PI * 2) { spriteBatch.DrawCircle(drawPos, circleRadius, 180, GUI.Style.Red, thickness: lineThickness); } @@ -510,13 +496,8 @@ namespace Barotrauma.Items.Components List availableAmmo = new List(); foreach (MapEntity e in item.linkedTo) { - var linkedItem = e as Item; - if (linkedItem == null) continue; - - var itemContainer = linkedItem.GetComponent(); - if (itemContainer?.Inventory?.Items == null) continue; - - availableAmmo.AddRange(itemContainer.Inventory.Items); + if (!(e is Item linkedItem)) { continue; } + availableAmmo.AddRange(linkedItem.ContainedItems); } float chargeRate = @@ -558,7 +539,7 @@ namespace Barotrauma.Items.Components { // TODO: Optimize? Creates multiple new objects per frame? Inventory.DrawSlot(spriteBatch, null, - new InventorySlot(new Rectangle(invSlotPos + new Point((i % slotsPerRow) * (slotSize.X + spacing), (int)Math.Floor(i / (float)slotsPerRow) * (slotSize.Y + spacing)), slotSize)), + new VisualSlot(new Rectangle(invSlotPos + new Point((i % slotsPerRow) * (slotSize.X + spacing), (int)Math.Floor(i / (float)slotsPerRow) * (slotSize.Y + spacing)), slotSize)), availableAmmo[i], -1, true); } if (flashNoAmmo) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index 13dd24e00..883248dc0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -9,7 +9,7 @@ using System.Linq; namespace Barotrauma { - class InventorySlot + class VisualSlot { public Rectangle Rect; @@ -66,7 +66,7 @@ namespace Barotrauma } } - public InventorySlot(Rectangle rect) + public VisualSlot(Rectangle rect) { Rect = rect; InteractRect = rect; @@ -91,21 +91,27 @@ namespace Barotrauma } HighlightScaleUpAmount = scaleUpAmount; - highlightCoroutine = CoroutineManager.StartCoroutine(UpdateBorderHighlight(color, fadeInDuration, fadeOutDuration)); + currentHighlightState = 0.0f; + this.fadeInDuration = fadeInDuration; + this.fadeOutDuration = fadeOutDuration; + currentHighlightColor = color; + HighlightTimer = 1.0f; + highlightCoroutine = CoroutineManager.StartCoroutine(UpdateBorderHighlight()); } - private IEnumerable UpdateBorderHighlight(Color color, float fadeInDuration, float fadeOutDuration) + private float currentHighlightState, fadeInDuration, fadeOutDuration; + private Color currentHighlightColor; + private IEnumerable UpdateBorderHighlight() { - float t = 0.0f; HighlightTimer = 1.0f; - while (t < fadeInDuration + fadeOutDuration) + while (currentHighlightState < fadeInDuration + fadeOutDuration) { - HighlightColor = (t < fadeInDuration) ? - Color.Lerp(Color.Transparent, color, t / fadeInDuration) : - Color.Lerp(color, Color.Transparent, (t - fadeInDuration) / fadeOutDuration); + HighlightColor = (currentHighlightState < fadeInDuration) ? + Color.Lerp(Color.Transparent, currentHighlightColor, currentHighlightState / fadeInDuration) : + Color.Lerp(currentHighlightColor, Color.Transparent, (currentHighlightState - fadeInDuration) / fadeOutDuration); - t += CoroutineManager.DeltaTime; - HighlightTimer = 1.0f - t / (fadeInDuration + fadeOutDuration); + currentHighlightState += CoroutineManager.DeltaTime; + HighlightTimer = 1.0f - currentHighlightState / (fadeInDuration + fadeOutDuration); yield return CoroutineStatus.Running; } @@ -115,6 +121,23 @@ namespace Barotrauma yield return CoroutineStatus.Success; } + + /// + /// Moves the current border highlight animation (if one is running) to the new slot + /// + public void MoveBorderHighlight(VisualSlot newSlot) + { + if (highlightCoroutine == null) { return; } + CoroutineManager.StopCoroutines(highlightCoroutine); + highlightCoroutine = null; + + newSlot.HighlightScaleUpAmount = HighlightScaleUpAmount; + newSlot.currentHighlightState = currentHighlightState; + newSlot.fadeInDuration = fadeInDuration; + newSlot.fadeOutDuration = fadeOutDuration; + newSlot.currentHighlightColor = currentHighlightColor; + newSlot.highlightCoroutine = CoroutineManager.StartCoroutine(newSlot.UpdateBorderHighlight()); + } } partial class Inventory @@ -152,6 +175,7 @@ namespace Barotrauma } public static Sprite DraggableIndicator; public static Sprite UnequippedIndicator, UnequippedHoverIndicator, UnequippedClickedIndicator, EquippedIndicator, EquippedHoverIndicator, EquippedClickedIndicator; + public static float IndicatorScaleAdjustment { get @@ -166,7 +190,7 @@ namespace Barotrauma public Rectangle BackgroundFrame { get; protected set; } - private ushort[] receivedItemIDs; + private List[] receivedItemIDs; private CoroutineHandle syncItemsCoroutine; public float HideTimer; @@ -186,26 +210,33 @@ namespace Barotrauma { public readonly Inventory ParentInventory; public readonly int SlotIndex; - public InventorySlot Slot; + public VisualSlot Slot; public Inventory Inventory; - public Item Item; - public bool IsSubSlot; + public readonly Item Item; + public readonly bool IsSubSlot; public string Tooltip; public List TooltipRichTextData; - public SlotReference(Inventory parentInventory, InventorySlot slot, int slotIndex, bool isSubSlot, Inventory subInventory = null) + public SlotReference(Inventory parentInventory, VisualSlot slot, int slotIndex, bool isSubSlot, Inventory subInventory = null) { ParentInventory = parentInventory; Slot = slot; SlotIndex = slotIndex; Inventory = subInventory; IsSubSlot = isSubSlot; - Item = ParentInventory.Items[slotIndex]; - TooltipRichTextData = RichTextData.GetRichTextData(GetTooltip(Item), out Tooltip); + Item = ParentInventory.GetItemAt(slotIndex); + + int stackCount = 1; + if (parentInventory != null && Item != null) + { + stackCount = parentInventory.GetItemsAt(slotIndex).Count(); + } + + TooltipRichTextData = RichTextData.GetRichTextData(GetTooltip(Item, stackCount), out Tooltip); } - private string GetTooltip(Item item) + private string GetTooltip(Item item, int stackCount) { if (item == null) { return null; } @@ -248,42 +279,50 @@ namespace Barotrauma } if (item.Prefab.ShowContentsInTooltip && item.OwnInventory != null) { - foreach (string itemName in item.OwnInventory.Items.Where(it => it != null).Select(it => it.Name).Distinct()) + foreach (string itemName in item.OwnInventory.AllItems.Select(it => it.Name).Distinct()) { - int itemCount = item.OwnInventory.Items.Count(it => it != null && it.Name == itemName); + int itemCount = item.OwnInventory.AllItems.Count(it => it != null && it.Name == itemName); description += itemCount == 1 ? "\n " + itemName : "\n " + itemName + " x" + itemCount; } } - toolTip = string.IsNullOrEmpty(description) ? - item.Name : - item.Name + '\n' + description; + string colorStr = XMLExtensions.ColorToString(item.SpawnedInOutpost ? GUI.Style.Red : Color.White); + + toolTip = $"‖color:{colorStr}‖{item.Name}‖color:end‖"; + if (!item.IsFullCondition && !item.Prefab.HideConditionBar) + { + string conditionColorStr = XMLExtensions.ColorToString(ToolBox.GradientLerp(item.Condition / item.MaxCondition, GUI.Style.ColorInventoryEmpty, GUI.Style.ColorInventoryHalf, GUI.Style.ColorInventoryFull)); + toolTip += $"‖color:{conditionColorStr}‖ ({(int)item.ConditionPercentage} %)‖color:end‖"; + } + if (!string.IsNullOrEmpty(description)) { toolTip += '\n' + description; } } - if (item.SpawnedInOutpost) + if (stackCount > 2) { - string colorStr = XMLExtensions.ColorToString(GUI.Style.Red); - toolTip = $"‖color:{colorStr}‖{toolTip}‖color:end‖"; + string colorStr = XMLExtensions.ColorToString(GUI.Style.Blue); + toolTip += $"\n‖color:{colorStr}‖[{GameMain.Config.KeyBindText(InputType.TakeOneFromInventorySlot)}] {TextManager.Get("inputtype.takeonefrominventoryslot")}‖color:end‖"; + colorStr = XMLExtensions.ColorToString(GUI.Style.Blue); + toolTip += $"\n‖color:{colorStr}‖[{GameMain.Config.KeyBindText(InputType.TakeHalfFromInventorySlot)}] {TextManager.Get("inputtype.takehalffrominventoryslot")}‖color:end‖"; } return toolTip; } } - public static InventorySlot draggingSlot; - public static Item draggingItem; + public static VisualSlot DraggingSlot; + public static readonly List DraggingItems = new List(); public static bool DraggingItemToWorld { get { - return draggingItem != null && + return DraggingItems.Any() && Character.Controlled != null && Character.Controlled.SelectedConstruction == null && CharacterHealth.OpenHealthWindow == null; } } - public static Item doubleClickedItem; + public static readonly List doubleClickedItems = new List(); protected Vector4 padding; @@ -294,11 +333,11 @@ namespace Barotrauma } protected static HashSet highlightedSubInventorySlots = new HashSet(); - private static List subInventorySlotsToDraw = new List(); + private static readonly List subInventorySlotsToDraw = new List(); protected static SlotReference selectedSlot; - public InventorySlot[] slots; + public VisualSlot[] visualSlots; private Rectangle prevRect; /// @@ -325,7 +364,7 @@ namespace Barotrauma public virtual void CreateSlots() { - slots = new InventorySlot[capacity]; + visualSlots = new VisualSlot[capacity]; int rows = (int)Math.Ceiling((double)capacity / slotsPerRow); int columns = Math.Min(slotsPerRow, capacity); @@ -366,24 +405,24 @@ namespace Barotrauma { slotRect.X = (int)(topLeft.X + (rectSize.X + spacing.X) * (i % slotsPerRow)); slotRect.Y = (int)(topLeft.Y + (rectSize.Y + spacing.Y) * ((int)Math.Floor((double)i / slotsPerRow))); - slots[i] = new InventorySlot(slotRect); - slots[i].InteractRect = new Rectangle( - (int)(slots[i].Rect.X - spacing.X / 2 - 1), (int)(slots[i].Rect.Y - spacing.Y / 2 - 1), - (int)(slots[i].Rect.Width + spacing.X + 2), (int)(slots[i].Rect.Height + spacing.Y + 2)); + visualSlots[i] = new VisualSlot(slotRect); + visualSlots[i].InteractRect = new Rectangle( + (int)(visualSlots[i].Rect.X - spacing.X / 2 - 1), (int)(visualSlots[i].Rect.Y - spacing.Y / 2 - 1), + (int)(visualSlots[i].Rect.Width + spacing.X + 2), (int)(visualSlots[i].Rect.Height + spacing.Y + 2)); - if (slots[i].Rect.Width > slots[i].Rect.Height) + if (visualSlots[i].Rect.Width > visualSlots[i].Rect.Height) { - slots[i].Rect.Inflate((slots[i].Rect.Height - slots[i].Rect.Width) / 2, 0); + visualSlots[i].Rect.Inflate((visualSlots[i].Rect.Height - visualSlots[i].Rect.Width) / 2, 0); } else { - slots[i].Rect.Inflate(0, (slots[i].Rect.Width - slots[i].Rect.Height) / 2); + visualSlots[i].Rect.Inflate(0, (visualSlots[i].Rect.Width - visualSlots[i].Rect.Height) / 2); } } if (selectedSlot != null && selectedSlot.ParentInventory == this) { - selectedSlot = new SlotReference(this, slots[selectedSlot.SlotIndex], selectedSlot.SlotIndex, selectedSlot.IsSubSlot, selectedSlot.Inventory); + selectedSlot = new SlotReference(this, visualSlots[selectedSlot.SlotIndex], selectedSlot.SlotIndex, selectedSlot.IsSubSlot, selectedSlot.Inventory); } CalculateBackgroundFrame(); } @@ -410,12 +449,12 @@ namespace Barotrauma protected virtual bool HideSlot(int i) { - return slots[i].Disabled || (hideEmptySlot[i] && Items[i] == null); + return visualSlots[i].Disabled || (slots[i].HideIfEmpty && slots[i].Empty()); } public virtual void Update(float deltaTime, Camera cam, bool subInventory = false) { - if (slots == null || isSubInventory != subInventory || + if (visualSlots == null || isSubInventory != subInventory || (RectTransform != null && RectTransform.Rect != prevRect)) { CreateSlots(); @@ -427,7 +466,7 @@ namespace Barotrauma for (int i = 0; i < capacity; i++) { if (HideSlot(i)) { continue; } - UpdateSlot(slots[i], i, Items[i], subInventory); + UpdateSlot(visualSlots[i], i, slots[i].Items.FirstOrDefault(), subInventory); } if (!isSubInventory) { @@ -445,7 +484,7 @@ namespace Barotrauma } } - protected void UpdateSlot(InventorySlot slot, int slotIndex, Item item, bool isSubSlot) + protected void UpdateSlot(VisualSlot slot, int slotIndex, Item item, bool isSubSlot) { Rectangle interactRect = slot.InteractRect; interactRect.Location += slot.DrawOffset.ToPoint(); @@ -465,7 +504,7 @@ namespace Barotrauma // Delete item from container in sub editor if (SubEditorScreen.IsSubEditor() && PlayerInput.IsCtrlDown()) { - draggingItem = null; + DraggingItems.Clear(); var mouseDrag = SubEditorScreen.MouseDragStart != Vector2.Zero && Vector2.Distance(PlayerInput.MousePosition, SubEditorScreen.MouseDragStart) >= GUI.Scale * 20; if (mouseOn && (PlayerInput.PrimaryMouseButtonClicked() || mouseDrag)) { @@ -510,7 +549,7 @@ namespace Barotrauma slot.State = GUIComponent.ComponentState.None; - if (mouseOn && (draggingItem != null || selectedSlot == null || selectedSlot.Slot == slot) && DraggingInventory == null) + if (mouseOn && (DraggingItems.Any() || selectedSlot == null || selectedSlot.Slot == slot) && DraggingInventory == null) // && //(highlightedSubInventories.Count == 0 || highlightedSubInventories.Contains(this) || highlightedSubInventorySlot?.Slot == slot || highlightedSubInventory.Owner == item)) { @@ -519,24 +558,47 @@ namespace Barotrauma if (selectedSlot == null || (!selectedSlot.IsSubSlot && isSubSlot)) { - var slotRef = new SlotReference(this, slot, slotIndex, isSubSlot, Items[slotIndex]?.GetComponent()?.Inventory); + var slotRef = new SlotReference(this, slot, slotIndex, isSubSlot, slots[slotIndex].FirstOrDefault()?.GetComponent()?.Inventory); if (Screen.Selected is SubEditorScreen editor && !editor.WiringMode && slotRef.ParentInventory is CharacterInventory) { return; } selectedSlot = slotRef; } - if (draggingItem == null) + if (!DraggingItems.Any()) { - if (PlayerInput.PrimaryMouseButtonDown()) + if (PlayerInput.PrimaryMouseButtonDown() && slots[slotIndex].Any()) { - draggingItem = Items[slotIndex]; - draggingSlot = slot; + if (PlayerInput.KeyDown(InputType.TakeHalfFromInventorySlot)) + { + DraggingItems.AddRange(slots[slotIndex].Items.Skip(slots[slotIndex].ItemCount / 2)); + } + else if (PlayerInput.KeyDown(InputType.TakeOneFromInventorySlot)) + { + DraggingItems.Add(slots[slotIndex].First()); + } + else + { + DraggingItems.AddRange(slots[slotIndex].Items); + } + DraggingSlot = slot; } } else if (PlayerInput.PrimaryMouseButtonReleased()) { - if (PlayerInput.DoubleClicked()) + if (PlayerInput.DoubleClicked() && slots[slotIndex].Any()) { - doubleClickedItem = item; + doubleClickedItems.Clear(); + if (PlayerInput.KeyDown(InputType.TakeHalfFromInventorySlot)) + { + doubleClickedItems.AddRange(slots[slotIndex].Items.Skip(slots[slotIndex].ItemCount / 2)); + } + else if (PlayerInput.KeyDown(InputType.TakeOneFromInventorySlot)) + { + doubleClickedItems.Add(slots[slotIndex].First()); + } + else + { + doubleClickedItems.AddRange(slots[slotIndex].Items); + } } } } @@ -544,11 +606,8 @@ namespace Barotrauma protected Inventory GetSubInventory(int slotIndex) { - var item = Items[slotIndex]; - if (item == null) return null; - - var container = item.GetComponent(); - if (container == null) return null; + var container = slots[slotIndex].FirstOrDefault()?.GetComponent(); + if (container == null) { return null; } return container.Inventory; } @@ -562,14 +621,14 @@ namespace Barotrauma public void UpdateSubInventory(float deltaTime, int slotIndex, Camera cam) { - var item = Items[slotIndex]; - if (item == null) return; + var item = slots[slotIndex].FirstOrDefault(); + if (item == null) { return; } var container = item.GetComponent(); - if (container == null || !container.DrawInventory) return; + if (container == null || !container.DrawInventory) { return; } var subInventory = container.Inventory; - if (subInventory.slots == null) subInventory.CreateSlots(); + if (subInventory.visualSlots == null) { subInventory.CreateSlots(); } canMove = container.MovableFrame && !subInventory.IsInventoryHoverAvailable(Owner as Character, container) && subInventory.originalPos != Point.Zero; @@ -588,8 +647,8 @@ namespace Barotrauma if (PlayerInput.PrimaryMouseButtonDown()) { // Prevent us from dragging an item - draggingItem = null; - draggingSlot = null; + DraggingItems.Clear(); + DraggingSlot = null; DraggingInventory = subInventory; } } @@ -605,8 +664,8 @@ namespace Barotrauma } } - int itemCapacity = subInventory.Items.Length; - var slot = slots[slotIndex]; + int itemCapacity = subInventory.slots.Length; + var slot = visualSlots[slotIndex]; int dir = slot.SubInventoryDir; Rectangle subRect = slot.Rect; Vector2 spacing; @@ -660,14 +719,14 @@ namespace Barotrauma for (int i = 0; i < itemCapacity; i++) { - subInventory.slots[i].Rect = subRect; - subInventory.slots[i].Rect.Location += new Point(0, (int)totalHeight * -dir); + subInventory.visualSlots[i].Rect = subRect; + subInventory.visualSlots[i].Rect.Location += new Point(0, (int)totalHeight * -dir); - subInventory.slots[i].DrawOffset = Vector2.SmoothStep(new Vector2(0, -50 * dir), new Vector2(0, totalHeight * dir), subInventory.OpenState); + subInventory.visualSlots[i].DrawOffset = Vector2.SmoothStep(new Vector2(0, -50 * dir), new Vector2(0, totalHeight * dir), subInventory.OpenState); - subInventory.slots[i].InteractRect = new Rectangle( - (int)(subInventory.slots[i].Rect.X - spacing.X / 2 - 1), (int)(subInventory.slots[i].Rect.Y - spacing.Y / 2 - 1), - (int)(subInventory.slots[i].Rect.Width + spacing.X + 2), (int)(subInventory.slots[i].Rect.Height + spacing.Y + 2)); + subInventory.visualSlots[i].InteractRect = new Rectangle( + (int)(subInventory.visualSlots[i].Rect.X - spacing.X / 2 - 1), (int)(subInventory.visualSlots[i].Rect.Y - spacing.Y / 2 - 1), + (int)(subInventory.visualSlots[i].Rect.Width + spacing.X + 2), (int)(subInventory.visualSlots[i].Rect.Height + spacing.Y + 2)); if ((i + 1) % columns == 0) { @@ -677,7 +736,7 @@ namespace Barotrauma } else { - subRect.X = (int)(subInventory.slots[i].Rect.Right + spacing.X); + subRect.X = (int)(subInventory.visualSlots[i].Rect.Right + spacing.X); } } @@ -686,7 +745,7 @@ namespace Barotrauma subInventory.movableFrameRect.X = subRect.X - (int)spacing.X; subInventory.movableFrameRect.Y = subRect.Y + (int)(spacing.Y); } - slots[slotIndex].State = GUIComponent.ComponentState.Hover; + visualSlots[slotIndex].State = GUIComponent.ComponentState.Hover; subInventory.isSubInventory = true; subInventory.Update(deltaTime, cam, true); @@ -694,7 +753,7 @@ namespace Barotrauma public void ClearSubInventories() { - if (highlightedSubInventorySlots.Count == 0) return; + if (highlightedSubInventorySlots.Count == 0) { return; } foreach (SlotReference highlightedSubInventorySlot in highlightedSubInventorySlots) { @@ -706,19 +765,16 @@ namespace Barotrauma public virtual void Draw(SpriteBatch spriteBatch, bool subInventory = false) { - if (slots == null || isSubInventory != subInventory) return; + if (visualSlots == null || isSubInventory != subInventory) { return; } for (int i = 0; i < capacity; i++) { - if (HideSlot(i)) continue; - - Rectangle interactRect = slots[i].InteractRect; - interactRect.Location += slots[i].DrawOffset.ToPoint(); + if (HideSlot(i)) { continue; } //don't draw the item if it's being dragged out of the slot - bool drawItem = draggingItem == null || draggingItem != Items[i] || interactRect.Contains(PlayerInput.MousePosition); + bool drawItem = !DraggingItems.Any() || !slots[i].Items.All(it => DraggingItems.Contains(it)) || visualSlots[i].MouseOn(); - DrawSlot(spriteBatch, this, slots[i], Items[i], i, drawItem); + DrawSlot(spriteBatch, this, visualSlots[i], slots[i].FirstOrDefault(), i, drawItem); } } @@ -727,7 +783,7 @@ namespace Barotrauma /// /// The desired slot we want to check /// True if our mouse is hover on the slot, false otherwise - public static bool IsMouseOnSlot(InventorySlot slot) + public static bool IsMouseOnSlot(VisualSlot slot) { var rect = new Rectangle(slot.InteractRect.X, slot.InteractRect.Y, slot.InteractRect.Width, slot.InteractRect.Height); rect.Offset(slot.DrawOffset); @@ -750,7 +806,7 @@ namespace Barotrauma if (!ignoreDraggedItem) { - if (draggingItem != null || DraggingInventory != null) { return true; } + if (DraggingItems.Any() || DraggingInventory != null) { return true; } } var isSubEditor = Screen.Selected is SubEditorScreen editor && !editor.WiringMode; @@ -758,9 +814,9 @@ namespace Barotrauma if (Character.Controlled.Inventory != null && !isSubEditor) { var inv = Character.Controlled.Inventory; - for (var i = 0; i < inv.slots.Length; i++) + for (var i = 0; i < inv.visualSlots.Length; i++) { - var slot = inv.slots[i]; + var slot = inv.visualSlots[i]; if (slot.InteractRect.Contains(PlayerInput.MousePosition)) { return true; @@ -768,8 +824,8 @@ namespace Barotrauma // check if the equip button actually exists if (slot.EquipButtonRect.Contains(PlayerInput.MousePosition) && - i >= 0 && inv.Items.Length > i && - inv.Items[i] != null) + i >= 0 && inv.slots.Length > i && + !inv.slots[i].Empty()) { return true; } @@ -779,9 +835,9 @@ namespace Barotrauma if (Character.Controlled.SelectedCharacter?.Inventory != null && !isSubEditor) { var inv = Character.Controlled.SelectedCharacter.Inventory; - for (var i = 0; i < inv.slots.Length; i++) + for (var i = 0; i < inv.visualSlots.Length; i++) { - var slot = inv.slots[i]; + var slot = inv.visualSlots[i]; if (slot.InteractRect.Contains(PlayerInput.MousePosition)) { return true; @@ -789,8 +845,8 @@ namespace Barotrauma // check if the equip button actually exists if (slot.EquipButtonRect.Contains(PlayerInput.MousePosition) && - i >= 0 && inv.Items.Length > i && - inv.Items[i] != null) + i >= 0 && inv.slots.Length > i && + !inv.slots[i].Empty()) { return true; } @@ -802,9 +858,9 @@ namespace Barotrauma foreach (var ic in Character.Controlled.SelectedConstruction.ActiveHUDs) { var itemContainer = ic as ItemContainer; - if (itemContainer?.Inventory?.slots == null) continue; + if (itemContainer?.Inventory?.visualSlots == null) { continue; } - foreach (InventorySlot slot in itemContainer.Inventory.slots) + foreach (VisualSlot slot in itemContainer.Inventory.visualSlots) { if (slot.InteractRect.Contains(PlayerInput.MousePosition) || slot.EquipButtonRect.Contains(PlayerInput.MousePosition)) @@ -817,7 +873,7 @@ namespace Barotrauma foreach (SlotReference highlightedSubInventorySlot in highlightedSubInventorySlots) { - if (GetSubInventoryHoverArea(highlightedSubInventorySlot).Contains(PlayerInput.MousePosition)) return true; + if (GetSubInventoryHoverArea(highlightedSubInventorySlot).Contains(PlayerInput.MousePosition)) { return true; } } return false; @@ -827,21 +883,21 @@ namespace Barotrauma { var character = Character.Controlled; if (character == null) { return CursorState.Default; } - if (draggingItem != null || DraggingInventory != null) { return CursorState.Dragging; } + if (DraggingItems.Any() || DraggingInventory != null) { return CursorState.Dragging; } var inv = character.Inventory; var selInv = character.SelectedCharacter?.Inventory; if (inv == null) { return CursorState.Default; } - foreach (var item in inv.Items) + foreach (var item in inv.AllItems) { var container = item?.GetComponent(); if (container == null) { continue; } - if (container.Inventory.slots != null) + if (container.Inventory.visualSlots != null) { - if (container.Inventory.slots.Any(slot => slot.IsHighlighted)) + if (container.Inventory.visualSlots.Any(slot => slot.IsHighlighted)) { return CursorState.Hand; } @@ -851,16 +907,16 @@ namespace Barotrauma { return CursorState.Move; } - } - + } if (selInv != null) { - for (int i = 0; i < selInv.slots.Length; i++) + for (int i = 0; i < selInv.visualSlots.Length; i++) { - InventorySlot slot = selInv.slots[i]; + VisualSlot slot = selInv.visualSlots[i]; + Item item = selInv.slots[i].FirstOrDefault(); if (slot.InteractRect.Contains(PlayerInput.MousePosition) || - (slot.EquipButtonRect.Contains(PlayerInput.MousePosition) && selInv.Items[i] != null && selInv.Items[i].AllowedSlots.Any(a => a == InvSlotType.Any))) + (slot.EquipButtonRect.Contains(PlayerInput.MousePosition) && item != null && item.AllowedSlots.Contains(InvSlotType.Any))) { return CursorState.Hand; } @@ -872,10 +928,10 @@ namespace Barotrauma foreach (var ic in character.SelectedConstruction.ActiveHUDs) { var itemContainer = ic as ItemContainer; - if (itemContainer?.Inventory?.slots == null) { continue; } - if (ic.Item.NonInteractable) { continue; } + if (itemContainer?.Inventory?.visualSlots == null) { continue; } + if (!ic.Item.IsInteractable(character)) { continue; } - foreach (var slot in itemContainer.Inventory.slots) + foreach (var slot in itemContainer.Inventory.visualSlots) { if (slot.InteractRect.Contains(PlayerInput.MousePosition) || slot.EquipButtonRect.Contains(PlayerInput.MousePosition)) @@ -886,10 +942,11 @@ namespace Barotrauma } } - for (int i = 0; i < inv.slots.Length; i++) + for (int i = 0; i < inv.visualSlots.Length; i++) { - InventorySlot slot = inv.slots[i]; - if (slot.EquipButtonRect.Contains(PlayerInput.MousePosition) && inv.Items[i] != null && inv.Items[i].AllowedSlots.Any(a => a == InvSlotType.Any)) + VisualSlot slot = inv.visualSlots[i]; + Item item = inv.slots[i].FirstOrDefault(); + if (slot.EquipButtonRect.Contains(PlayerInput.MousePosition) && item != null && item.AllowedSlots.Contains(InvSlotType.Any)) { return CursorState.Hand; } @@ -915,20 +972,20 @@ namespace Barotrauma public void DrawSubInventory(SpriteBatch spriteBatch, int slotIndex) { - var item = Items[slotIndex]; - if (item == null) return; + var item = slots[slotIndex].FirstOrDefault(); + if (item == null) { return; } var container = item.GetComponent(); - if (container == null || !container.DrawInventory) return; + if (container == null || !container.DrawInventory) { return; } - if (container.Inventory.slots == null || !container.Inventory.isSubInventory) return; + if (container.Inventory.visualSlots == null || !container.Inventory.isSubInventory) { return; } int itemCapacity = container.Capacity; #if DEBUG - System.Diagnostics.Debug.Assert(slotIndex >= 0 && slotIndex < Items.Length); + System.Diagnostics.Debug.Assert(slotIndex >= 0 && slotIndex < slots.Length); #else - if (slotIndex < 0 || slotIndex >= Items.Length) return; + if (slotIndex < 0 || slotIndex >= capacity) { return; } #endif if (!canMove) @@ -936,17 +993,17 @@ namespace Barotrauma Rectangle prevScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle; spriteBatch.End(); spriteBatch.Begin(SpriteSortMode.Deferred, rasterizerState: GameMain.ScissorTestEnable); - if (slots[slotIndex].SubInventoryDir > 0) + if (visualSlots[slotIndex].SubInventoryDir > 0) { spriteBatch.GraphicsDevice.ScissorRectangle = new Rectangle( - new Point(0, slots[slotIndex].Rect.Bottom), - new Point(GameMain.GraphicsWidth, (int)Math.Max(GameMain.GraphicsHeight - slots[slotIndex].Rect.Bottom, 0))); + new Point(0, visualSlots[slotIndex].Rect.Bottom), + new Point(GameMain.GraphicsWidth, (int)Math.Max(GameMain.GraphicsHeight - visualSlots[slotIndex].Rect.Bottom, 0))); } else { spriteBatch.GraphicsDevice.ScissorRectangle = new Rectangle( new Point(0, 0), - new Point(GameMain.GraphicsWidth, slots[slotIndex].Rect.Y)); + new Point(GameMain.GraphicsWidth, visualSlots[slotIndex].Rect.Y)); } container.Inventory.Draw(spriteBatch, true); spriteBatch.End(); @@ -959,13 +1016,13 @@ namespace Barotrauma } container.InventoryBottomSprite?.Draw(spriteBatch, - new Vector2(slots[slotIndex].Rect.Center.X, slots[slotIndex].Rect.Y) + slots[slotIndex].DrawOffset, + new Vector2(visualSlots[slotIndex].Rect.Center.X, visualSlots[slotIndex].Rect.Y) + visualSlots[slotIndex].DrawOffset, 0.0f, UIScale); container.InventoryTopSprite?.Draw(spriteBatch, new Vector2( - slots[slotIndex].Rect.Center.X, - container.Inventory.slots[container.Inventory.slots.Length - 1].Rect.Y) + container.Inventory.slots[container.Inventory.slots.Length - 1].DrawOffset, + visualSlots[slotIndex].Rect.Center.X, + container.Inventory.visualSlots[container.Inventory.visualSlots.Length - 1].Rect.Y) + container.Inventory.visualSlots[container.Inventory.visualSlots.Length - 1].DrawOffset, 0.0f, UIScale); if (container.MovableFrame && !IsInventoryHoverAvailable(Owner as Character, container)) @@ -1002,16 +1059,37 @@ namespace Barotrauma public static void UpdateDragging() { - if (draggingItem != null && PlayerInput.PrimaryMouseButtonReleased()) + if (DraggingItems.Any() && PlayerInput.PrimaryMouseButtonReleased()) { Character.Controlled.ClearInputs(); if (!IsMouseOnInventory(ignoreDraggedItem: true) && - CharacterHealth.OpenHealthWindow != null && - CharacterHealth.OpenHealthWindow.OnItemDropped(draggingItem, false)) + CharacterHealth.OpenHealthWindow != null) { - draggingItem = null; - return; + bool dropSuccessful = false; + foreach (Item item in DraggingItems) + { + var inventory = item.ParentInventory; + var indices = inventory?.FindIndices(item); + dropSuccessful |= CharacterHealth.OpenHealthWindow.OnItemDropped(item, false); + if (dropSuccessful) + { + if (indices != null && inventory.visualSlots != null) + { + foreach (int i in indices) + { + inventory.visualSlots[i]?.ShowBorderHighlight(GUI.Style.Green, 0.1f, 0.4f); + } + } + break; + } + + } + if (dropSuccessful) + { + DraggingItems.Clear(); + return; + } } if (selectedSlot == null) @@ -1019,18 +1097,24 @@ namespace Barotrauma if (DraggingItemToWorld && Character.Controlled.FocusedItem?.OwnInventory != null && (Character.Controlled.FocusedItem.GetComponent()?.HasRequiredItems(Character.Controlled, addMessage: false) ?? false) && - Character.Controlled.FocusedItem.OwnInventory.CanBePut(draggingItem) && - Character.Controlled.FocusedItem.OwnInventory.TryPutItem(draggingItem, Character.Controlled)) + Character.Controlled.FocusedItem.OwnInventory.CanBePut(DraggingItems.FirstOrDefault())) { - SoundPlayer.PlayUISound(GUISoundType.PickItem); + bool anySuccess = false; + foreach (Item it in DraggingItems) + { + bool success = Character.Controlled.FocusedItem.OwnInventory.TryPutItem(it, Character.Controlled); + if (!success) { break; } + anySuccess |= success; + } + if (anySuccess) { SoundPlayer.PlayUISound(GUISoundType.PickItem); } } else { if (Screen.Selected is SubEditorScreen) { - if (draggingItem?.ParentInventory != null) + if (DraggingItems.First()?.ParentInventory != null) { - SubEditorScreen.StoreCommand(new InventoryPlaceCommand(draggingItem.ParentInventory, new List { draggingItem }, true)); + SubEditorScreen.StoreCommand(new InventoryPlaceCommand(DraggingItems.First().ParentInventory, new List(DraggingItems), true)); } } @@ -1040,95 +1124,130 @@ namespace Barotrauma { if (editor.EntityMenu.Rect.Contains(PlayerInput.MousePosition)) { - draggingItem.Remove(); + DraggingItems.ForEachMod(it => it.Remove()); removed = true; } else { if (editor.WiringMode) { - draggingItem.Remove(); + DraggingItems.ForEachMod(it => it.Remove()); removed = true; } else { - draggingItem.Drop(Character.Controlled); + DraggingItems.ForEachMod(it => it.Drop(Character.Controlled)); } } } else { - draggingItem.Drop(Character.Controlled); + DraggingItems.ForEachMod(it => it.Drop(Character.Controlled)); } SoundPlayer.PlayUISound(removed ? GUISoundType.PickItem : GUISoundType.DropItem); } } - else if (selectedSlot.ParentInventory.Items[selectedSlot.SlotIndex] != draggingItem) + else if (!DraggingItems.Any(it => selectedSlot.ParentInventory.slots[selectedSlot.SlotIndex].Contains(it))) { - Inventory oldInventory = draggingItem.ParentInventory; + Inventory oldInventory = DraggingItems.First().ParentInventory; Inventory selectedInventory = selectedSlot.ParentInventory; int slotIndex = selectedSlot.SlotIndex; - int oldSlot = oldInventory == null ? 0 : Array.IndexOf(oldInventory.Items, draggingItem); + int oldSlot = oldInventory == null ? 0 : Array.IndexOf(oldInventory.slots, DraggingItems); //if attempting to drop into an invalid slot in the same inventory, try to move to the correct slot - if (selectedInventory.Items[slotIndex] == null && + if (selectedInventory.slots[slotIndex].Empty() && selectedInventory == Character.Controlled.Inventory && - !draggingItem.AllowedSlots.Any(a => a.HasFlag(Character.Controlled.Inventory.SlotTypes[slotIndex])) && - selectedInventory.TryPutItem(draggingItem, Character.Controlled, draggingItem.AllowedSlots)) + !DraggingItems.First().AllowedSlots.Any(a => a.HasFlag(Character.Controlled.Inventory.SlotTypes[slotIndex])) && + DraggingItems.Any(it => selectedInventory.TryPutItem(it, Character.Controlled, it.AllowedSlots))) { - if (selectedInventory.slots != null) + if (selectedInventory.visualSlots != null) { - for (int i = 0; i < selectedInventory.slots.Length; i++) + for (int i = 0; i < selectedInventory.visualSlots.Length; i++) { - if (selectedInventory.Items[i] == draggingItem) + if (DraggingItems.Any(it => selectedInventory.slots[i].Contains(it))) { - selectedInventory.slots[slotIndex].ShowBorderHighlight(Color.White, 0.1f, 0.4f); + selectedInventory.visualSlots[slotIndex].ShowBorderHighlight(Color.White, 0.1f, 0.4f); } } - selectedInventory.slots[slotIndex].ShowBorderHighlight(GUI.Style.Red, 0.1f, 0.9f); + selectedInventory.visualSlots[slotIndex].ShowBorderHighlight(GUI.Style.Red, 0.1f, 0.9f); } SoundPlayer.PlayUISound(GUISoundType.PickItem); } - else if (selectedInventory.TryPutItem(draggingItem, slotIndex, true, true, Character.Controlled)) - { - if (SubEditorScreen.IsSubEditor()) - { - SubEditorScreen.StoreCommand(new InventoryMoveCommand(oldInventory, selectedInventory, draggingItem, oldSlot, slotIndex)); - } - if (selectedInventory.slots != null) { selectedInventory.slots[slotIndex].ShowBorderHighlight(Color.White, 0.1f, 0.4f); } - SoundPlayer.PlayUISound(GUISoundType.PickItem); - } else { - if (selectedInventory.slots != null){ selectedInventory.slots[slotIndex].ShowBorderHighlight(GUI.Style.Red, 0.1f, 0.9f); } - SoundPlayer.PlayUISound(GUISoundType.PickItemFail); + bool anySuccess = false; + foreach (Item item in DraggingItems) + { + bool success = selectedInventory.TryPutItem(item, slotIndex, allowSwapping: !anySuccess, true, Character.Controlled); + anySuccess |= success; + if (!success) { break; } + } + + if (anySuccess) + { + highlightedSubInventorySlots.RemoveWhere(s => s.ParentInventory == oldInventory || s.ParentInventory == selectedInventory); + if (SubEditorScreen.IsSubEditor()) + { + foreach (Item draggingItem in DraggingItems) + { + if (selectedInventory.slots[slotIndex].Contains(draggingItem)) + { + SubEditorScreen.StoreCommand(new InventoryMoveCommand(oldInventory, selectedInventory, draggingItem, oldSlot, slotIndex)); + } + } + } + if (selectedInventory.visualSlots != null) { selectedInventory.visualSlots[slotIndex].ShowBorderHighlight(Color.White, 0.1f, 0.4f); } + SoundPlayer.PlayUISound(GUISoundType.PickItem); + } + else + { + if (selectedInventory.visualSlots != null){ selectedInventory.visualSlots[slotIndex].ShowBorderHighlight(GUI.Style.Red, 0.1f, 0.9f); } + SoundPlayer.PlayUISound(GUISoundType.PickItemFail); + } } + selectedInventory.HideTimer = 2.0f; if (selectedSlot.ParentInventory?.Owner is Item parentItem && parentItem.ParentInventory != null) { for (int i = 0; i < parentItem.ParentInventory.capacity; i++) { - if (parentItem.ParentInventory.HideSlot(i)) continue; - if (parentItem.ParentInventory.Items[i] != parentItem) continue; + if (parentItem.ParentInventory.HideSlot(i)) { continue; } + if (parentItem.ParentInventory.slots[i].FirstOrDefault() != parentItem) { continue; } highlightedSubInventorySlots.Add(new SlotReference( - parentItem.ParentInventory, parentItem.ParentInventory.slots[i], + parentItem.ParentInventory, parentItem.ParentInventory.visualSlots[i], i, false, selectedSlot.ParentInventory)); break; } } - draggingItem = null; - draggingSlot = null; + DraggingItems.Clear(); + DraggingSlot = null; } - draggingItem = null; + DraggingItems.Clear(); } - if (selectedSlot != null && !selectedSlot.Slot.MouseOn()) + if (selectedSlot != null) { - selectedSlot = null; + if (!selectedSlot.Slot.MouseOn()) + { + selectedSlot = null; + } + else + { + var rootOwner = (selectedSlot.ParentInventory?.Owner as Item)?.GetRootInventoryOwner(); + if (selectedSlot.ParentInventory?.Owner != Character.Controlled && + selectedSlot.ParentInventory?.Owner != Character.Controlled.SelectedCharacter && + selectedSlot.ParentInventory?.Owner != Character.Controlled.SelectedConstruction && + rootOwner != Character.Controlled && + rootOwner != Character.Controlled.SelectedCharacter && + rootOwner != Character.Controlled.SelectedConstruction) + { + selectedSlot = null; + } + } } } @@ -1148,9 +1267,9 @@ namespace Barotrauma hoverArea = Rectangle.Union(hoverArea, subSlot.Inventory.movableFrameRect); } - if (subSlot.Inventory?.slots != null) + if (subSlot.Inventory?.visualSlots != null) { - foreach (InventorySlot slot in subSlot.Inventory.slots) + foreach (VisualSlot slot in subSlot.Inventory.visualSlots) { Rectangle subSlotRect = slot.InteractRect; subSlotRect.Location += slot.DrawOffset.ToPoint(); @@ -1186,18 +1305,19 @@ namespace Barotrauma subInventorySlotsToDraw.AddRange(highlightedSubInventorySlots); foreach (var slot in subInventorySlotsToDraw) { - int slotIndex = Array.IndexOf(slot.ParentInventory.slots, slot.Slot); - if (slotIndex > -1 && slotIndex < slot.ParentInventory.slots.Length) + int slotIndex = Array.IndexOf(slot.ParentInventory.visualSlots, slot.Slot); + if (slotIndex > -1 && slotIndex < slot.ParentInventory.visualSlots.Length && + (slot.Item?.GetComponent()?.HasRequiredItems(Character.Controlled, addMessage: false) ?? true)) { slot.ParentInventory.DrawSubInventory(spriteBatch, slotIndex); } } - if (draggingItem != null) + if (DraggingItems.Any()) { - if (draggingSlot == null || (!draggingSlot.MouseOn())) + if (DraggingSlot == null || (!DraggingSlot.MouseOn())) { - Sprite sprite = draggingItem.Prefab.InventoryIcon ?? draggingItem.Sprite; + Sprite sprite = DraggingItems.First().Prefab.InventoryIcon ?? DraggingItems.First().Sprite; int iconSize = (int)(64 * GUI.Scale); float scale = Math.Min(Math.Min(iconSize / sprite.size.X, iconSize / sprite.size.Y), 1.5f); @@ -1212,12 +1332,12 @@ namespace Barotrauma Character.Controlled.FocusedItem != null ? TextManager.GetWithVariable("PutItemIn", "[itemname]", Character.Controlled.FocusedItem.Name, true) : TextManager.Get(Screen.Selected is SubEditorScreen editor && editor.EntityMenu.Rect.Contains(PlayerInput.MousePosition) ? "Delete" : "DropItem"); - int textWidth = (int)Math.Max(GUI.Font.MeasureString(draggingItem.Name).X, GUI.SmallFont.MeasureString(toolTip).X); + int textWidth = (int)Math.Max(GUI.Font.MeasureString(DraggingItems.First().Name).X, GUI.SmallFont.MeasureString(toolTip).X); int textSpacing = (int)(15 * GUI.Scale); Point shadowBorders = (new Point(40, 10)).Multiply(GUI.Scale); shadowSprite.Draw(spriteBatch, new Rectangle(itemPos.ToPoint() - new Point(iconSize / 2) - shadowBorders, new Point(iconSize + textWidth + textSpacing, iconSize) + shadowBorders.Multiply(2)), Color.Black * 0.8f); - GUI.DrawString(spriteBatch, new Vector2(itemPos.X + iconSize / 2 + textSpacing, itemPos.Y - iconSize / 2), draggingItem.Name, Color.White); + GUI.DrawString(spriteBatch, new Vector2(itemPos.X + iconSize / 2 + textSpacing, itemPos.Y - iconSize / 2), DraggingItems.First().Name, Color.White); GUI.DrawString(spriteBatch, new Vector2(itemPos.X + iconSize / 2 + textSpacing, itemPos.Y), toolTip, color: Character.Controlled.FocusedItem == null && !mouseOnHealthInterface ? GUI.Style.Red : Color.LightGreen, font: GUI.SmallFont); @@ -1225,8 +1345,16 @@ namespace Barotrauma sprite.Draw(spriteBatch, itemPos + Vector2.One * 2, Color.Black, scale: scale); sprite.Draw(spriteBatch, itemPos, - sprite == draggingItem.Sprite ? draggingItem.GetSpriteColor() : draggingItem.GetInventoryIconColor(), + sprite == DraggingItems.First().Sprite ? DraggingItems.First().GetSpriteColor() : DraggingItems.First().GetInventoryIconColor(), scale: scale); + + if (DraggingItems.First().Prefab.MaxStackSize > 1) + { + Vector2 stackCountPos = itemPos + Vector2.One * iconSize * 0.25f; + string stackCountText = "x" + DraggingItems.Count; + GUI.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos + Vector2.One, Color.Black); + GUI.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos, Color.White); + } } } @@ -1238,7 +1366,7 @@ namespace Barotrauma } } - public static void DrawSlot(SpriteBatch spriteBatch, Inventory inventory, InventorySlot slot, Item item, int slotIndex, bool drawItem = true, InvSlotType type = InvSlotType.Any) + public static void DrawSlot(SpriteBatch spriteBatch, Inventory inventory, VisualSlot slot, Item item, int slotIndex, bool drawItem = true, InvSlotType type = InvSlotType.Any) { Rectangle rect = slot.Rect; rect.Location += slot.DrawOffset.ToPoint(); @@ -1250,7 +1378,7 @@ namespace Barotrauma } Color slotColor = Color.White; - if ((inventory?.Owner as Item)?.NonInteractable ?? false) { slotColor = Color.Gray; } + if (inventory?.Owner is Item i && !i.IsPlayerTeamInteractable) { slotColor = Color.Gray; } var itemContainer = item?.GetComponent(); if (itemContainer != null && (itemContainer.InventoryTopSprite != null || itemContainer.InventoryBottomSprite != null)) { @@ -1286,19 +1414,19 @@ namespace Barotrauma bool canBePut = false; - if (draggingItem != null && inventory != null && slotIndex > -1 && slotIndex < inventory.slots.Length) + if (DraggingItems.Any() && inventory != null && slotIndex > -1 && slotIndex < inventory.visualSlots.Length) { - if (inventory.CanBePut(draggingItem, slotIndex)) + if (inventory.CanBePut(DraggingItems.First(), slotIndex)) { canBePut = true; } - else if (inventory.Items[slotIndex]?.OwnInventory?.CanBePut(draggingItem) ?? false) + else if (inventory.slots[slotIndex].FirstOrDefault()?.OwnInventory?.CanBePut(DraggingItems.First()) ?? false) { canBePut = true; } - else if (inventory.Items[slotIndex] == null && inventory == Character.Controlled.Inventory && - !draggingItem.AllowedSlots.Any(a => a.HasFlag(Character.Controlled.Inventory.SlotTypes[slotIndex])) && - Character.Controlled.Inventory.CanBeAutoMovedToCorrectSlots(draggingItem)) + else if (inventory.slots[slotIndex] == null && inventory == Character.Controlled.Inventory && + !DraggingItems.First().AllowedSlots.Any(a => a.HasFlag(Character.Controlled.Inventory.SlotTypes[slotIndex])) && + Character.Controlled.Inventory.CanBeAutoMovedToCorrectSlots(DraggingItems.First())) { canBePut = true; } @@ -1312,10 +1440,24 @@ namespace Barotrauma { if (!item.IsFullCondition && !item.Prefab.HideConditionBar && (itemContainer == null || !itemContainer.ShowConditionInContainedStateIndicator)) { - GUI.DrawRectangle(spriteBatch, new Rectangle(rect.X, rect.Bottom - 8, rect.Width, 8), Color.Black * 0.8f, true); - GUI.DrawRectangle(spriteBatch, - new Rectangle(rect.X, rect.Bottom - 8, (int)(rect.Width * (item.Condition / item.MaxCondition)), 8), - Color.Lerp(GUI.Style.Red, GUI.Style.Green, item.Condition / item.MaxCondition) * 0.8f, true); + int dir = slot.SubInventoryDir; + Rectangle conditionIndicatorArea; + if (itemContainer != null && itemContainer.ShowContainedStateIndicator) + { + conditionIndicatorArea = new Rectangle(rect.X, rect.Bottom - (int)(10 * GUI.Scale), rect.Width, (int)(10 * GUI.Scale)); + } + else + { + conditionIndicatorArea = new Rectangle( + rect.X, dir < 0 ? rect.Bottom + HUDLayoutSettings.Padding / 2 : rect.Y - HUDLayoutSettings.Padding / 2 - ContainedIndicatorHeight, + rect.Width, ContainedIndicatorHeight); + conditionIndicatorArea.Inflate(-4, 0); + } + + var indicatorStyle = GUI.Style.GetComponentStyle("ContainedStateIndicator.Default"); + Sprite indicatorSprite = indicatorStyle?.GetDefaultSprite(); + Sprite emptyIndicatorSprite = indicatorStyle?.GetSprite(GUIComponent.ComponentState.Hover); + DrawItemStateIndicator(spriteBatch, inventory, indicatorSprite, emptyIndicatorSprite, conditionIndicatorArea, item.Condition / item.MaxCondition); } if (itemContainer != null && itemContainer.ShowContainedStateIndicator) @@ -1327,9 +1469,18 @@ namespace Barotrauma } else { + var containedItem = itemContainer.Inventory.slots[0].FirstOrDefault(); containedState = itemContainer.Inventory.Capacity == 1 ? - (itemContainer.Inventory.Items[0] == null ? 0.0f : itemContainer.Inventory.Items[0].Condition / itemContainer.Inventory.Items[0].MaxCondition) : - itemContainer.Inventory.Items.Count(i => i != null) / (float)itemContainer.Inventory.capacity; + (containedItem == null ? 0.0f : containedItem.Condition / containedItem.MaxCondition) : + itemContainer.Inventory.slots.Count(i => !i.Empty()) / (float)itemContainer.Inventory.capacity; + if (containedItem != null && itemContainer.Inventory.Capacity == 1) + { + int maxStackSize = Math.Min(containedItem.Prefab.MaxStackSize, itemContainer.MaxStackSize); + if (maxStackSize > 1) + { + containedState = itemContainer.Inventory.slots[0].ItemCount / (float)maxStackSize; + } + } } int dir = slot.SubInventoryDir; @@ -1337,57 +1488,17 @@ namespace Barotrauma dir < 0 ? rect.Bottom + HUDLayoutSettings.Padding / 2 : rect.Y - HUDLayoutSettings.Padding / 2 - ContainedIndicatorHeight, rect.Width, ContainedIndicatorHeight); containedIndicatorArea.Inflate(-4, 0); - Color backgroundColor = GUI.Style.ColorInventoryBackground; + Sprite indicatorSprite = + itemContainer.ContainedStateIndicator ?? + itemContainer.IndicatorStyle?.GetDefaultSprite(); + Sprite emptyIndicatorSprite = + itemContainer.ContainedStateIndicatorEmpty ?? + itemContainer.IndicatorStyle?.GetSprite(GUIComponent.ComponentState.Hover); - if (itemContainer.ContainedStateIndicator?.Texture == null) - { - containedIndicatorArea.Inflate(0, -2); - GUI.DrawRectangle(spriteBatch, containedIndicatorArea, backgroundColor, true); - GUI.DrawRectangle(spriteBatch, - new Rectangle(containedIndicatorArea.X, containedIndicatorArea.Y, (int)(containedIndicatorArea.Width * containedState), containedIndicatorArea.Height), - ToolBox.GradientLerp(containedState, GUI.Style.ColorInventoryEmpty, GUI.Style.ColorInventoryHalf, GUI.Style.ColorInventoryFull) * 0.8f, true); - GUI.DrawLine(spriteBatch, - new Vector2(containedIndicatorArea.X + (int)(containedIndicatorArea.Width * containedState), containedIndicatorArea.Y), - new Vector2(containedIndicatorArea.X + (int)(containedIndicatorArea.Width * containedState), containedIndicatorArea.Bottom), - Color.Black * 0.8f); - } - else - { - Sprite indicatorSprite = itemContainer.ContainedStateIndicator; - float indicatorScale = Math.Min( - containedIndicatorArea.Width / (float)indicatorSprite.SourceRect.Width, - containedIndicatorArea.Height / (float)indicatorSprite.SourceRect.Height); + bool usingDefaultSprite = itemContainer.IndicatorStyle?.Name == "ContainedStateIndicator.Default"; - if (containedState >= 0.0f && containedState < 0.25f && inventory == Character.Controlled?.Inventory && Character.Controlled.HasEquippedItem(item)) - { - indicatorScale += ((float)Math.Sin(Timing.TotalTime * 5.0f) + 1.0f) * 0.25f; - } - - indicatorSprite.Draw(spriteBatch, containedIndicatorArea.Center.ToVector2(), - (inventory != null && inventory.Locked) ? backgroundColor * 0.5f : backgroundColor, - origin: indicatorSprite.size / 2, - rotate: 0.0f, - scale: indicatorScale); - - Color indicatorColor = ToolBox.GradientLerp(containedState, GUI.Style.ColorInventoryEmpty, GUI.Style.ColorInventoryHalf, GUI.Style.ColorInventoryFull); - if (inventory != null && inventory.Locked) { indicatorColor *= 0.5f; } - - spriteBatch.Draw(indicatorSprite.Texture, containedIndicatorArea.Center.ToVector2(), - sourceRectangle: new Rectangle(indicatorSprite.SourceRect.Location, new Point((int)(indicatorSprite.SourceRect.Width * containedState), indicatorSprite.SourceRect.Height)), - color: indicatorColor, - rotation: 0.0f, - origin: indicatorSprite.size / 2, - scale: indicatorScale, - effects: SpriteEffects.None, layerDepth: 0.0f); - - spriteBatch.Draw(indicatorSprite.Texture, containedIndicatorArea.Center.ToVector2(), - sourceRectangle: new Rectangle(indicatorSprite.SourceRect.X - 1 + (int)(indicatorSprite.SourceRect.Width * containedState), indicatorSprite.SourceRect.Y, Math.Max((int)Math.Ceiling(1 / indicatorScale), 2), indicatorSprite.SourceRect.Height), - color: Color.Black, - rotation: 0.0f, - origin: new Vector2(indicatorSprite.size.X * (0.5f - containedState), indicatorSprite.size.Y * 0.5f), - scale: indicatorScale, - effects: SpriteEffects.None, layerDepth: 0.0f); - } + DrawItemStateIndicator(spriteBatch, inventory, indicatorSprite, emptyIndicatorSprite, containedIndicatorArea, containedState, + pulsate: !usingDefaultSprite && containedState >= 0.0f && containedState < 0.25f && inventory == Character.Controlled?.Inventory && Character.Controlled.HasEquippedItem(item)); } } } @@ -1438,11 +1549,28 @@ namespace Barotrauma var stealIcon = CharacterInventory.LimbSlotIcons[InvSlotType.LeftHand]; Vector2 iconSize = new Vector2(25 * GUI.Scale); stealIcon.Draw( - spriteBatch, + spriteBatch, new Vector2(rect.X + iconSize.X * 0.2f, rect.Bottom - iconSize.Y * 1.2f), color: GUI.Style.Red, scale: iconSize.X / stealIcon.size.X); } + int maxStackSize = item.Prefab.MaxStackSize; + if (item.Container != null) + { + maxStackSize = Math.Min(maxStackSize, item.Container.GetComponent()?.MaxStackSize ?? maxStackSize); + } + if (maxStackSize > 1) + { + int itemCount = slot.MouseOn() ? inventory.slots[slotIndex].ItemCount : inventory.slots[slotIndex].Items.Where(it => !DraggingItems.Contains(it)).Count(); + if (item.IsFullCondition || MathUtils.NearlyEqual(item.Condition, 0.0f) || itemCount > 1) + { + Vector2 stackCountPos = new Vector2(rect.Right, rect.Bottom); + string stackCountText = "x" + itemCount; + stackCountPos -= GUI.SmallFont.MeasureString(stackCountText) + new Vector2(4, 2); + GUI.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos + Vector2.One, Color.Black); + GUI.SmallFont.DrawString(spriteBatch, stackCountText, stackCountPos, Color.White); + } + } } if (inventory != null && @@ -1455,14 +1583,90 @@ namespace Barotrauma } } + private static void DrawItemStateIndicator( + SpriteBatch spriteBatch, Inventory inventory, + Sprite indicatorSprite, Sprite emptyIndicatorSprite, Rectangle containedIndicatorArea, float containedState, + bool pulsate = false) + { + Color backgroundColor = GUI.Style.ColorInventoryBackground; + + if (indicatorSprite == null) + { + containedIndicatorArea.Inflate(0, -2); + GUI.DrawRectangle(spriteBatch, containedIndicatorArea, backgroundColor, true); + GUI.DrawRectangle(spriteBatch, + new Rectangle(containedIndicatorArea.X, containedIndicatorArea.Y, (int)(containedIndicatorArea.Width * containedState), containedIndicatorArea.Height), + ToolBox.GradientLerp(containedState, GUI.Style.ColorInventoryEmpty, GUI.Style.ColorInventoryHalf, GUI.Style.ColorInventoryFull) * 0.8f, true); + GUI.DrawLine(spriteBatch, + new Vector2(containedIndicatorArea.X + (int)(containedIndicatorArea.Width * containedState), containedIndicatorArea.Y), + new Vector2(containedIndicatorArea.X + (int)(containedIndicatorArea.Width * containedState), containedIndicatorArea.Bottom), + Color.Black * 0.8f); + } + else + { + float indicatorScale = Math.Min( + containedIndicatorArea.Width / (float)indicatorSprite.SourceRect.Width, + containedIndicatorArea.Height / (float)indicatorSprite.SourceRect.Height); + + if (pulsate) + { + indicatorScale += ((float)Math.Sin(Timing.TotalTime * 5.0f) + 1.0f) * 0.2f; + } + + indicatorSprite.Draw(spriteBatch, containedIndicatorArea.Center.ToVector2(), + (inventory != null && inventory.Locked) ? backgroundColor * 0.5f : backgroundColor, + origin: indicatorSprite.size / 2, + rotate: 0.0f, + scale: indicatorScale); + + if (containedState > 0.0f) + { + Color indicatorColor = ToolBox.GradientLerp(containedState, GUI.Style.ColorInventoryEmpty, GUI.Style.ColorInventoryHalf, GUI.Style.ColorInventoryFull); + if (inventory != null && inventory.Locked) { indicatorColor *= 0.5f; } + + spriteBatch.Draw(indicatorSprite.Texture, containedIndicatorArea.Center.ToVector2(), + sourceRectangle: new Rectangle(indicatorSprite.SourceRect.Location, new Point((int)(indicatorSprite.SourceRect.Width * containedState), indicatorSprite.SourceRect.Height)), + color: indicatorColor, + rotation: 0.0f, + origin: indicatorSprite.size / 2, + scale: indicatorScale, + effects: SpriteEffects.None, layerDepth: 0.0f); + + spriteBatch.Draw(indicatorSprite.Texture, containedIndicatorArea.Center.ToVector2(), + sourceRectangle: new Rectangle(indicatorSprite.SourceRect.X - 1 + (int)(indicatorSprite.SourceRect.Width * containedState), indicatorSprite.SourceRect.Y, Math.Max((int)Math.Ceiling(1 / indicatorScale), 2), indicatorSprite.SourceRect.Height), + color: Color.Black, + rotation: 0.0f, + origin: new Vector2(indicatorSprite.size.X * (0.5f - containedState), indicatorSprite.size.Y * 0.5f), + scale: indicatorScale, + effects: SpriteEffects.None, layerDepth: 0.0f); + } + else if (emptyIndicatorSprite != null) + { + Color indicatorColor = GUI.Style.ColorInventoryEmptyOverlay; + if (inventory != null && inventory.Locked) { indicatorColor *= 0.5f; } + + emptyIndicatorSprite.Draw(spriteBatch, containedIndicatorArea.Center.ToVector2(), + indicatorColor, + origin: emptyIndicatorSprite.size / 2, + rotate: 0.0f, + scale: indicatorScale); + } + } + } + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { UInt16 lastEventID = msg.ReadUInt16(); - byte itemCount = msg.ReadByte(); - receivedItemIDs = new ushort[itemCount]; - for (int i = 0; i < itemCount; i++) + byte slotCount = msg.ReadByte(); + receivedItemIDs = new List[slotCount]; + for (int i = 0; i < slotCount; i++) { - receivedItemIDs[i] = msg.ReadUInt16(); + receivedItemIDs[i] = new List(); + int itemCount = msg.ReadRangedInteger(0, MaxStackSize); + for (int j = 0; j < itemCount; j++) + { + receivedItemIDs[i].Add(msg.ReadUInt16()); + } } //delay applying the new state if less than 1 second has passed since this client last sent a state to the server @@ -1517,26 +1721,31 @@ namespace Barotrauma for (int i = 0; i < capacity; i++) { - if (receivedItemIDs[i] == 0 || (Entity.FindEntityByID(receivedItemIDs[i]) as Item != Items[i])) + foreach (Item item in slots[i].Items.ToList()) { - Items[i]?.Drop(null); - System.Diagnostics.Debug.Assert(Items[i] == null); + if (!receivedItemIDs[i].Contains(item.ID)) + { + item.Drop(null); + } } } //iterate backwards to get the item to the Any slots first for (int i = capacity - 1; i >= 0; i--) { - if (receivedItemIDs[i] > 0) + if (!receivedItemIDs[i].Any()) { continue; } + foreach (UInt16 id in receivedItemIDs[i]) { - if (!(Entity.FindEntityByID(receivedItemIDs[i]) is Item item) || Items[i] == item) { continue; } - - TryPutItem(item, i, false, false, null, false); + if (!(Entity.FindEntityByID(id) is Item item) || slots[i].Contains(item)) { continue; } + if (!TryPutItem(item, i, false, false, null, false)) + { + ForceToSlot(item, i); + } for (int j = 0; j < capacity; j++) { - if (Items[j] == item && receivedItemIDs[j] != item.ID) + if (slots[j].Contains(item) && !receivedItemIDs[j].Contains(item.ID)) { - Items[j] = null; + slots[j].RemoveItem(item); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index 46826b42e..ac63d80d2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.Linq; using Barotrauma.Extensions; +using Barotrauma.MapCreatures.Behavior; using FarseerPhysics.Dynamics; using FarseerPhysics.Dynamics.Contacts; @@ -15,7 +16,7 @@ namespace Barotrauma { partial class Item : MapEntity, IDamageable, ISerializableEntity, IServerSerializable, IClientSerializable { - public static bool ShowItems = true; + public static bool ShowItems = true, ShowWires = true; private readonly List positionBuffer = new List(); @@ -29,6 +30,7 @@ namespace Barotrauma private bool editingHUDRefreshPending; private float editingHUDRefreshTimer; + private ContainedItemSprite activeContainedSprite; private readonly Dictionary spriteAnimState = new Dictionary(); @@ -103,13 +105,10 @@ namespace Barotrauma Color color = spriteColor; if (Prefab.UseContainedSpriteColor && ownInventory != null) { - for (int i = 0; i < ownInventory.Items.Length; i++) + foreach (Item item in ContainedItems) { - if (ownInventory.Items[i] != null) - { - color = ownInventory.Items[i].ContainerColor; - break; - } + color = item.ContainerColor; + break; } } return color; @@ -120,13 +119,10 @@ namespace Barotrauma Color color = InventoryIconColor; if (Prefab.UseContainedInventoryIconColor && ownInventory != null) { - for (int i = 0; i < ownInventory.Items.Length; i++) + foreach (Item item in ContainedItems) { - if (ownInventory.Items[i] != null) - { - color = ownInventory.Items[i].ContainerColor; - break; - } + color = item.ContainerColor; + break; } } return color; @@ -135,6 +131,7 @@ namespace Barotrauma partial void SetActiveSpriteProjSpecific() { activeSprite = prefab.sprite; + activeContainedSprite = null; Holdable holdable = GetComponent(); if (holdable != null && holdable.Attached) { @@ -142,7 +139,9 @@ namespace Barotrauma { if (containedSprite.UseWhenAttached) { - activeSprite = containedSprite.Sprite; + activeContainedSprite = containedSprite; + activeSprite = containedSprite.Sprite; + UpdateSpriteStates(0.0f); return; } } @@ -154,7 +153,9 @@ namespace Barotrauma { if (containedSprite.MatchesContainer(Container)) { + activeContainedSprite = containedSprite; activeSprite = containedSprite.Sprite; + UpdateSpriteStates(0.0f); return; } } @@ -187,6 +188,7 @@ namespace Barotrauma decorativeSprite.Sprite.EnsureLazyLoaded(); spriteAnimState.Add(decorativeSprite, new DecorativeSprite.State()); } + SetActiveSprite(); UpdateSpriteStates(0.0f); } @@ -240,7 +242,15 @@ namespace Barotrauma public override void Draw(SpriteBatch spriteBatch, bool editing, bool back = true) { if (!Visible || (!editing && HiddenInGame)) { return; } - if (editing && !ShowItems) { return; } + + if (editing) + { + if (isWire) + { + if (!ShowWires) { return; } + } + else if (!ShowItems) { return; } + } Color color = IsHighlighted && !GUI.DisableItemHighlights && Screen.Selected != GameMain.GameScreen ? GUI.Style.Orange : GetSpriteColor(); //if (IsSelected && editing) color = Color.Lerp(color, Color.Gold, 0.5f); @@ -305,7 +315,7 @@ namespace Barotrauma foreach (var decorativeSprite in Prefab.DecorativeSprites) { if (!spriteAnimState[decorativeSprite].IsActive) { continue; } - Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, -rotationRad) * Scale; + Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, -rotationRad) * Scale; if (flippedX && Prefab.CanSpriteFlipX) { offset.X = -offset.X; } if (flippedY && Prefab.CanSpriteFlipY) { offset.Y = -offset.Y; } decorativeSprite.Sprite.DrawTiled(spriteBatch, @@ -328,7 +338,7 @@ namespace Barotrauma } activeSprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + drawOffset, color, origin, rotationRad, Scale, activeSprite.effects, depth); fadeInBrokenSprite?.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + fadeInBrokenSprite.Offset.ToVector2() * Scale, color * fadeInBrokenSpriteAlpha, origin, rotationRad, Scale, activeSprite.effects, depth - 0.000001f); - if (Infector != null && Infector.ParentBallastFlora.HasBrokenThrough) + if (Infector != null && (Infector.ParentBallastFlora.HasBrokenThrough || BallastFloraBehavior.AlwaysShowBallastFloraSprite)) { Prefab.InfectedSprite?.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + drawOffset, color, Prefab.InfectedSprite.Origin, rotationRad, Scale, activeSprite.effects, depth - 0.001f); Prefab.DamagedInfectedSprite?.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + drawOffset, Infector.HealthColor, Prefab.DamagedInfectedSprite.Origin, rotationRad, Scale, activeSprite.effects, depth - 0.002f); @@ -336,12 +346,12 @@ namespace Barotrauma foreach (var decorativeSprite in Prefab.DecorativeSprites) { if (!spriteAnimState[decorativeSprite].IsActive) { continue; } - float rot = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState); - Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, -rotationRad) * Scale; + float rot = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState, spriteAnimState[decorativeSprite].RandomRotationFactor); + Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, -rotationRad) * Scale; if (flippedX && Prefab.CanSpriteFlipX) { offset.X = -offset.X; } if (flippedY && Prefab.CanSpriteFlipY) { offset.Y = -offset.Y; } decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + offset.X, -(DrawPosition.Y + offset.Y)), color, - rotationRad + rot, decorativeSprite.Scale * Scale, activeSprite.effects, + rotationRad + rot, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, activeSprite.effects, depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth), 0.999f)); } } @@ -353,7 +363,7 @@ namespace Barotrauma { if (!back) { return; } float depthStep = 0.000001f; - if (holdable.Picker.SelectedItems[0] == this) + if (holdable.Picker.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand) == this) { Limb holdLimb = holdable.Picker.AnimController.GetLimb(LimbType.RightHand); if (holdLimb?.ActiveSprite != null) @@ -365,7 +375,7 @@ namespace Barotrauma } } } - else if (holdable.Picker.SelectedItems[1] == this) + else if (holdable.Picker.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand) == this) { Limb holdLimb = holdable.Picker.AnimController.GetLimb(LimbType.LeftHand); if (holdLimb?.ActiveSprite != null) @@ -384,8 +394,8 @@ namespace Barotrauma foreach (var decorativeSprite in Prefab.DecorativeSprites) { if (!spriteAnimState[decorativeSprite].IsActive) { continue; } - float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState); - Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, -rotationRad) * Scale; + float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState, spriteAnimState[decorativeSprite].RandomRotationFactor); + Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, -rotationRad) * Scale; if (flippedX && Prefab.CanSpriteFlipX) { offset.X = -offset.X; } if (flippedY && Prefab.CanSpriteFlipY) { offset.Y = -offset.Y; } var ca = (float)Math.Cos(-body.Rotation); @@ -393,7 +403,7 @@ namespace Barotrauma Vector2 transformedOffset = new Vector2(ca * offset.X + sa * offset.Y, -sa * offset.X + ca * offset.Y); decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + transformedOffset.X, -(DrawPosition.Y + transformedOffset.Y)), color, - -body.Rotation + rotation, decorativeSprite.Scale * Scale, activeSprite.effects, + -body.Rotation + rotation, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, activeSprite.effects, depth: depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth)); } } @@ -405,12 +415,12 @@ namespace Barotrauma foreach (var decorativeSprite in upgradeSprites) { if (!spriteAnimState[decorativeSprite].IsActive) { continue; } - float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState); - Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, -rotationRad) * Scale; + float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState, spriteAnimState[decorativeSprite].RandomRotationFactor); + Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, -rotationRad) * Scale; if (flippedX && Prefab.CanSpriteFlipX) { offset.X = -offset.X; } if (flippedY && Prefab.CanSpriteFlipY) { offset.Y = -offset.Y; } decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + offset.X, -(DrawPosition.Y + offset.Y)), color, - rotation, decorativeSprite.Scale * Scale, activeSprite.effects, + rotation, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, activeSprite.effects, depth: depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth)); } @@ -505,12 +515,39 @@ namespace Barotrauma public void UpdateSpriteStates(float deltaTime) { - DecorativeSprite.UpdateSpriteStates(Prefab.DecorativeSpriteGroups, spriteAnimState, ID, deltaTime, ConditionalMatches); + if (activeContainedSprite != null) + { + if (activeContainedSprite.DecorativeSpriteBehavior == ContainedItemSprite.DecorativeSpriteBehaviorType.HideWhenVisible) + { + foreach (DecorativeSprite decorativeSprite in Prefab.DecorativeSprites) + { + var spriteState = spriteAnimState[decorativeSprite]; + spriteState.IsActive = false; + } + return; + } + } + else + { + foreach (var containedSprite in Prefab.ContainedSprites) + { + if (containedSprite.Sprite != activeSprite && containedSprite.DecorativeSpriteBehavior == ContainedItemSprite.DecorativeSpriteBehaviorType.HideWhenNotVisible) + { + foreach (DecorativeSprite decorativeSprite in Prefab.DecorativeSprites) + { + var spriteState = spriteAnimState[decorativeSprite]; + spriteState.IsActive = false; + } + return; + } + } + } + + DecorativeSprite.UpdateSpriteStates(Prefab.DecorativeSpriteGroups, spriteAnimState, ID, deltaTime, ConditionalMatches); foreach (var upgrade in Upgrades) { - var upgradeSprites = GetUpgradeSprites(upgrade); - + var upgradeSprites = GetUpgradeSprites(upgrade); foreach (var decorativeSprite in upgradeSprites) { var spriteState = spriteAnimState[decorativeSprite]; @@ -1035,6 +1072,28 @@ namespace Barotrauma // Always create the texts if they have not yet been created if (texts.Any() && !recreateHudTexts) { return texts; } texts.Clear(); + + string nameText = Name; + if (Prefab.Identifier == "idcard" || Tags.Contains("despawncontainer")) + { + string[] readTags = Tags.Split(','); + string idName = null; + foreach (string tag in readTags) + { + string[] s = tag.Split(':'); + if (s[0] == "name") + { + idName = s[1]; + break; + } + } + if (idName != null) + { + nameText += $" ({idName})"; + } + } + texts.Add(new ColoredText(nameText, GUI.Style.TextColor, false, false)); + foreach (ItemComponent ic in components) { if (string.IsNullOrEmpty(ic.DisplayMsg)) { continue; } @@ -1259,7 +1318,7 @@ namespace Barotrauma { if (GameMain.Client == null) { return; } - if (parentInventory != null || body == null || !body.Enabled || Removed) + if (parentInventory != null || body == null || !body.Enabled || Removed || (GetComponent()?.IsStuckToTarget ?? false)) { positionBuffer.Clear(); return; @@ -1267,12 +1326,7 @@ namespace Barotrauma isActive = true; - Vector2 newVelocity = body.LinearVelocity; - Vector2 newPosition = body.SimPosition; - float newAngularVelocity = body.AngularVelocity; - float newRotation = body.Rotation; - body.CorrectPosition(positionBuffer, out newPosition, out newVelocity, out newRotation, out newAngularVelocity); - + body.CorrectPosition(positionBuffer, out Vector2 newPosition, out Vector2 newVelocity, out float newRotation, out float newAngularVelocity); body.LinearVelocity = newVelocity; body.AngularVelocity = newAngularVelocity; if (Vector2.DistanceSquared(newPosition, body.SimPosition) > 0.0001f || @@ -1490,7 +1544,7 @@ namespace Barotrauma foreach (WifiComponent wifiComponent in item.GetComponents()) { - wifiComponent.TeamID = (Character.TeamType)teamID; + wifiComponent.TeamID = (CharacterTeamType)teamID; } if (descriptionChanged) { item.Description = itemDesc; } if (tagsChanged) { item.Tags = tags; } @@ -1521,10 +1575,10 @@ namespace Barotrauma partial void RemoveProjSpecific() { - if (Inventory.draggingItem == this) + if (Inventory.DraggingItems.Contains(this)) { - Inventory.draggingItem = null; - Inventory.draggingSlot = null; + Inventory.DraggingItems.Clear(); + Inventory.DraggingSlot = null; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemInventory.cs index d637d1aa6..20ab00f8a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemInventory.cs @@ -24,14 +24,14 @@ namespace Barotrauma protected override void CalculateBackgroundFrame() { - var firstSlot = slots.FirstOrDefault(); + var firstSlot = visualSlots.FirstOrDefault(); if (firstSlot == null) { return; } Rectangle frame = firstSlot.Rect; frame.Location += firstSlot.DrawOffset.ToPoint(); for (int i = 1; i < capacity; i++) { - Rectangle slotRect = slots[i].Rect; - slotRect.Location += slots[i].DrawOffset.ToPoint(); + Rectangle slotRect = visualSlots[i].Rect; + slotRect.Location += visualSlots[i].DrawOffset.ToPoint(); frame = Rectangle.Union(frame, slotRect); } BackgroundFrame = new Rectangle( @@ -43,7 +43,7 @@ namespace Barotrauma public override void Draw(SpriteBatch spriteBatch, bool subInventory = false) { - if (slots != null && slots.Length > 0) + if (visualSlots != null && visualSlots.Length > 0) { CalculateBackgroundFrame(); if (container.InventoryBackSprite == null) @@ -70,7 +70,7 @@ namespace Barotrauma if (container.InventoryBottomSprite != null && !subInventory) { container.InventoryBottomSprite.Draw(spriteBatch, - new Vector2(BackgroundFrame.Center.X, BackgroundFrame.Bottom) + slots[0].DrawOffset, + new Vector2(BackgroundFrame.Center.X, BackgroundFrame.Bottom) + visualSlots[0].DrawOffset, 0.0f, UIScale); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs index b8896c2ae..2e9d73a1c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs @@ -27,8 +27,14 @@ namespace Barotrauma class ContainedItemSprite { + public enum DecorativeSpriteBehaviorType + { + None, HideWhenVisible, HideWhenNotVisible + } + public readonly Sprite Sprite; public readonly bool UseWhenAttached; + public readonly DecorativeSpriteBehaviorType DecorativeSpriteBehavior; public readonly string[] AllowedContainerIdentifiers; public readonly string[] AllowedContainerTags; @@ -36,6 +42,7 @@ namespace Barotrauma { Sprite = new Sprite(element, path, lazyLoad: lazyLoad); UseWhenAttached = element.GetAttributeBool("usewhenattached", false); + Enum.TryParse(element.GetAttributeString("decorativespritebehavior", "None"), ignoreCase: true, out DecorativeSpriteBehavior); AllowedContainerIdentifiers = element.GetAttributeStringArray("allowedcontaineridentifiers", new string[0], convertToLowerInvariant: true); AllowedContainerTags = element.GetAttributeStringArray("allowedcontainertags", new string[0], convertToLowerInvariant: true); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs index 59e59c823..540d896d0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs @@ -65,6 +65,8 @@ namespace Barotrauma.MapCreatures.Behavior public readonly List DamageParticles = new List(); public readonly List DeathParticles = new List(); + public static bool AlwaysShowBallastFloraSprite = false; + partial void LoadPrefab(XElement element) { string? branchAtlasPath = element.GetAttributeString("branchatlas", null); @@ -125,7 +127,7 @@ namespace Barotrauma.MapCreatures.Behavior float particleAmount = Rand.Range(16, 32); for (int i = 0; i < particleAmount; i++) { - GameMain.ParticleManager.CreateParticle("shrapnel", pos, Rand.Vector(Rand.Range(-50f, 50.0f))); + GameMain.ParticleManager.CreateParticle("shrapnel", pos, Rand.Vector(Rand.Range(0f, 250.0f)), Rand.Range(0f, 360.0f)); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs index 4bf8e79fb..87c8a8a64 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs @@ -43,10 +43,10 @@ namespace Barotrauma } } - public void DrawFront(SpriteBatch spriteBatch, Camera cam) + public void DrawDebugOverlay(SpriteBatch spriteBatch, Camera cam) { if (renderer == null) { return; } - renderer.Draw(spriteBatch, cam); + renderer.DrawDebugOverlay(spriteBatch, cam); if (GameMain.DebugDraw && Screen.Selected.Cam.Zoom > 0.1f) { @@ -114,10 +114,13 @@ namespace Barotrauma graphics.Clear(BackgroundColor); - if (renderer == null) return; - renderer.DrawBackground(spriteBatch, cam, LevelObjectManager, backgroundCreatureManager); + renderer?.DrawBackground(spriteBatch, cam, LevelObjectManager, backgroundCreatureManager); } + public void DrawFront(SpriteBatch spriteBatch, Camera cam) + { + renderer?.DrawForeground(spriteBatch, cam, LevelObjectManager); + } public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { bool isGlobalUpdate = msg.ReadBoolean(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs index ca4822b95..41525b40c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs @@ -241,6 +241,7 @@ namespace Barotrauma private void UpdateDeformations(float deltaTime) { + if (ActivePrefab.DeformableSprite == null) { return; } foreach (SpriteDeformation deformation in spriteDeformations) { if (deformation is PositionalDeformation positionalDeformation) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs index 73ff19b6f..a25d03711 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -10,6 +10,7 @@ namespace Barotrauma partial class LevelObjectManager { private readonly List visibleObjectsBack = new List(); + private readonly List visibleObjectsMid = new List(); private readonly List visibleObjectsFront = new List(); private double NextRefreshTime; @@ -26,6 +27,10 @@ namespace Barotrauma { obj.Update(deltaTime); } + foreach (LevelObject obj in visibleObjectsMid) + { + obj.Update(deltaTime); + } foreach (LevelObject obj in visibleObjectsFront) { obj.Update(deltaTime); @@ -34,7 +39,7 @@ namespace Barotrauma public IEnumerable GetVisibleObjects() { - return visibleObjectsBack.Union(visibleObjectsFront); + return visibleObjectsBack.Union(visibleObjectsMid).Union(visibleObjectsFront); } /// @@ -43,6 +48,7 @@ namespace Barotrauma private void RefreshVisibleObjects(Rectangle currentIndices, float zoom) { visibleObjectsBack.Clear(); + visibleObjectsMid.Clear(); visibleObjectsFront.Clear(); float minSizeToDraw = MathHelper.Lerp(10.0f, 5.0f, Math.Min(zoom * 20.0f, 1.0f)); @@ -70,7 +76,10 @@ namespace Barotrauma } } - var objectList = obj.Position.Z >= 0 ? visibleObjectsBack : visibleObjectsFront; + var objectList = + obj.Position.Z >= 0 ? + visibleObjectsBack : + (obj.Position.Z < -1 ? visibleObjectsFront : visibleObjectsMid); int drawOrderIndex = 0; for (int i = 0; i < objectList.Count; i++) { @@ -102,8 +111,31 @@ namespace Barotrauma currentGridIndices = currentIndices; } + /// + /// Draw the objects behind the level walls + /// + public void DrawObjectsBack(SpriteBatch spriteBatch, Camera cam) + { + DrawObjects(spriteBatch, cam, visibleObjectsBack); + } - public void DrawObjects(SpriteBatch spriteBatch, Camera cam, bool drawFront) + /// + /// Draw the objects in front of the level walls, but behind characters + /// + public void DrawObjectsMid(SpriteBatch spriteBatch, Camera cam) + { + DrawObjects(spriteBatch, cam, visibleObjectsMid); + } + + /// + /// Draw the objects in front of the level walls and characters + /// + public void DrawObjectsFront(SpriteBatch spriteBatch, Camera cam) + { + DrawObjects(spriteBatch, cam, visibleObjectsFront); + } + + private void DrawObjects(SpriteBatch spriteBatch, Camera cam, List objectList) { Rectangle indices = Rectangle.Empty; indices.X = (int)Math.Floor(cam.WorldView.X / (float)GridSize); @@ -132,7 +164,6 @@ namespace Barotrauma } } - var objectList = drawFront ? visibleObjectsFront : visibleObjectsBack; foreach (LevelObject obj in objectList) { Vector2 camDiff = new Vector2(obj.Position.X, obj.Position.Y) - cam.WorldViewCenter; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs index 4944630cc..8537fd04d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs @@ -220,7 +220,7 @@ namespace Barotrauma SamplerState.LinearWrap, DepthStencilState.DepthRead, null, null, cam.Transform); - backgroundSpriteManager?.DrawObjects(spriteBatch, cam, drawFront: false); + backgroundSpriteManager?.DrawObjectsBack(spriteBatch, cam); if (cam.Zoom > 0.05f) { backgroundCreatureManager?.Draw(spriteBatch, cam); @@ -262,8 +262,6 @@ namespace Barotrauma color: Color.White * alpha, textureScale: new Vector2(texScale)); } } - - spriteBatch.End(); RenderWalls(GameMain.Instance.GraphicsDevice, cam); @@ -272,11 +270,21 @@ namespace Barotrauma BlendState.NonPremultiplied, SamplerState.LinearClamp, DepthStencilState.DepthRead, null, null, cam.Transform); - if (backgroundSpriteManager != null) backgroundSpriteManager.DrawObjects(spriteBatch, cam, drawFront: true); + backgroundSpriteManager?.DrawObjectsMid(spriteBatch, cam); spriteBatch.End(); } - public void Draw(SpriteBatch spriteBatch, Camera cam) + public void DrawForeground(SpriteBatch spriteBatch, Camera cam, LevelObjectManager backgroundSpriteManager = null) + { + spriteBatch.Begin(SpriteSortMode.Deferred, + BlendState.NonPremultiplied, + SamplerState.LinearClamp, DepthStencilState.DepthRead, null, null, + cam.Transform); + backgroundSpriteManager?.DrawObjectsFront(spriteBatch, cam); + spriteBatch.End(); + } + + public void DrawDebugOverlay(SpriteBatch spriteBatch, Camera cam) { if (GameMain.DebugDraw && cam.Zoom > 0.1f) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs index 7f207a77e..6565af460 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs @@ -391,7 +391,7 @@ namespace Barotrauma.Lights if (GUI.DisableItemHighlights) { return false; } highlightedEntities.Clear(); - if (Character.Controlled != null && (!Character.Controlled.IsKeyDown(InputType.Aim) || Character.Controlled.SelectedItems.Any(it => it?.GetComponent() == null))) + if (Character.Controlled != null && (!Character.Controlled.IsKeyDown(InputType.Aim) || Character.Controlled.HeldItems.Any(it => it.GetComponent() == null))) { if (Character.Controlled.FocusedItem != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs index d0f7bace4..b7d9dfe51 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs @@ -222,7 +222,7 @@ namespace Barotrauma return !tileDiscovered[MathHelper.Clamp(x, 0, tileDiscovered.Length), MathHelper.Clamp(y, 0, tileDiscovered.Length)]; } - partial void ChangeLocationType(Location location, string prevName, LocationTypeChange change) + partial void ChangeLocationTypeProjSpecific(Location location, string prevName, LocationTypeChange change) { if (change.Messages.Any()) { @@ -383,11 +383,11 @@ namespace Barotrauma Level.Loaded.DebugSetEndLocation(null); CurrentLocation.Discovered = true; - CurrentLocation.CreateStore(); OnLocationChanged?.Invoke(prevLocation, CurrentLocation); SelectLocation(-1); if (GameMain.Client == null) { + CurrentLocation.CreateStore(); ProgressWorld(); } else @@ -550,7 +550,7 @@ namespace Barotrauma location.Type.Sprite.Draw(spriteBatch, pos, color, scale: generationParams.LocationIconSize / location.Type.Sprite.size.X * iconScale * zoom); - if (location.TypeChangeTimer <= 0 && !string.IsNullOrEmpty(location.LastTypeChangeMessage) && generationParams.TypeChangeIcon != null) + if (location.TimeSinceLastTypeChange < 1 && !string.IsNullOrEmpty(location.LastTypeChangeMessage) && generationParams.TypeChangeIcon != null) { Vector2 typeChangeIconPos = pos + new Vector2(1.35f, -0.35f) * generationParams.LocationIconSize * 0.5f * zoom; float typeChangeIconScale = 18.0f / generationParams.TypeChangeIcon.SourceRect.Width; @@ -610,7 +610,7 @@ namespace Barotrauma Vector2 nameSize = GUI.LargeFont.MeasureString(HighlightedLocation.Name); Vector2 typeSize = GUI.Font.MeasureString(HighlightedLocation.Type.Name); Vector2 size = new Vector2(Math.Max(nameSize.X, typeSize.X), nameSize.Y + typeSize.Y); - bool showReputation = HighlightedLocation.Discovered && HighlightedLocation.Type.HasOutpost && HighlightedLocation.Reputation != null; + bool showReputation = hudVisibility > 0.0f && HighlightedLocation.Discovered && HighlightedLocation.Type.HasOutpost && HighlightedLocation.Reputation != null; string repLabelText = null, repValueText = null; Vector2 repLabelSize = Vector2.Zero, repBarSize = Vector2.Zero; if (showReputation) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs index eec523790..b9cbf543f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs @@ -214,7 +214,7 @@ namespace Barotrauma } else if (PlayerInput.KeyHit(Keys.V)) { - Paste(cam.WorldViewCenter); + Paste(cam.ScreenToWorld(PlayerInput.MousePosition)); } else if (PlayerInput.KeyHit(Keys.G)) { @@ -279,31 +279,10 @@ namespace Barotrauma if (GUI.KeyboardDispatcher.Subscriber == null) { - int up = PlayerInput.KeyDown(Keys.Up) ? 1 : 0, - down = PlayerInput.KeyDown(Keys.Down) ? -1 : 0, - left = PlayerInput.KeyDown(Keys.Left) ? -1 : 0, - right = PlayerInput.KeyDown(Keys.Right) ? 1 : 0; - - int xKeysDown = (left + right); - int yKeysDown = (up + down); - - if (xKeysDown != 0 || yKeysDown != 0) { keyDelay += (float) Timing.Step; } else { keyDelay = 0; } - - Vector2 nudgeAmount = Vector2.Zero; - - if (keyDelay >= 0.5f) + Vector2 nudge = GetNudgeAmount(); + if (nudge != Vector2.Zero) { - nudgeAmount.Y = yKeysDown; - nudgeAmount.X = xKeysDown; - } - - if (PlayerInput.KeyHit(Keys.Up)) nudgeAmount.Y = 1f; - if (PlayerInput.KeyHit(Keys.Down)) nudgeAmount.Y = -1f; - if (PlayerInput.KeyHit(Keys.Left)) nudgeAmount.X = -1f; - if (PlayerInput.KeyHit(Keys.Right)) nudgeAmount.X = 1f; - if (nudgeAmount != Vector2.Zero) - { - foreach (MapEntity entityToNudge in selectedList) { entityToNudge.Move(nudgeAmount); } + foreach (MapEntity entityToNudge in selectedList) { entityToNudge.Move(nudge); } } } else @@ -476,6 +455,8 @@ namespace Barotrauma { if (PlayerInput.PrimaryMouseButtonHeld() && PlayerInput.KeyUp(Keys.Space) && + PlayerInput.KeyUp(Keys.LeftAlt) && + PlayerInput.KeyUp(Keys.RightAlt) && (highlightedListBox == null || (GUI.MouseOn != highlightedListBox && !highlightedListBox.IsParentOf(GUI.MouseOn)))) { //if clicking a selected entity, start moving it @@ -491,6 +472,37 @@ namespace Barotrauma } } + public static Vector2 GetNudgeAmount(bool doHold = true) + { + Vector2 nudgeAmount = Vector2.Zero; + if (doHold) + { + int up = PlayerInput.KeyDown(Keys.Up) ? 1 : 0, + down = PlayerInput.KeyDown(Keys.Down) ? -1 : 0, + left = PlayerInput.KeyDown(Keys.Left) ? -1 : 0, + right = PlayerInput.KeyDown(Keys.Right) ? 1 : 0; + + int xKeysDown = (left + right); + int yKeysDown = (up + down); + + if (xKeysDown != 0 || yKeysDown != 0) { keyDelay += (float) Timing.Step; } else { keyDelay = 0; } + + + if (keyDelay >= 0.5f) + { + nudgeAmount.Y = yKeysDown; + nudgeAmount.X = xKeysDown; + } + } + + if (PlayerInput.KeyHit(Keys.Up)) nudgeAmount.Y = 1f; + if (PlayerInput.KeyHit(Keys.Down)) nudgeAmount.Y = -1f; + if (PlayerInput.KeyHit(Keys.Left)) nudgeAmount.X = -1f; + if (PlayerInput.KeyHit(Keys.Right)) nudgeAmount.X = 1f; + + return nudgeAmount; + } + public MapEntity GetReplacementOrThis() { return ReplacedBy?.GetReplacementOrThis() ?? this; @@ -511,7 +523,7 @@ namespace Barotrauma { if (entities == null) { - if (potentialContainer.OwnInventory != null && potentialContainer.ParentInventory == null && !potentialContainer.OwnInventory.IsFull()) + if (potentialContainer.OwnInventory != null && potentialContainer.ParentInventory == null && !potentialContainer.OwnInventory.IsFull(takeStacksIntoAccount: true)) { targetContainer = potentialContainer; break; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntityPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntityPrefab.cs index 2347db53c..e7adb8e8d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntityPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntityPrefab.cs @@ -45,7 +45,10 @@ namespace Barotrauma { CreateInstance(newRect); placePosition = Vector2.Zero; - selected = null; + if (!PlayerInput.IsShiftDown()) + { + selected = null; + } } newRect.Y = -newRect.Y; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs index 98cfec6fd..d1effedfe 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -191,10 +191,11 @@ namespace Barotrauma Vector2 max = new Vector2(worldRect.Right, worldRect.Y); foreach (DecorativeSprite decorativeSprite in Prefab.DecorativeSprites) { - min.X = Math.Min(worldPos.X - decorativeSprite.Sprite.size.X * decorativeSprite.Sprite.RelativeOrigin.X * decorativeSprite.Scale * Scale, min.X); - max.X = Math.Max(worldPos.X + decorativeSprite.Sprite.size.X * (1.0f - decorativeSprite.Sprite.RelativeOrigin.X) * decorativeSprite.Scale * Scale, max.X); - min.Y = Math.Min(worldPos.Y - decorativeSprite.Sprite.size.Y * (1.0f - decorativeSprite.Sprite.RelativeOrigin.Y) * decorativeSprite.Scale * Scale, min.Y); - max.Y = Math.Max(worldPos.Y + decorativeSprite.Sprite.size.Y * decorativeSprite.Sprite.RelativeOrigin.Y * decorativeSprite.Scale * Scale, max.Y); + float scale = decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale; + min.X = Math.Min(worldPos.X - decorativeSprite.Sprite.size.X * decorativeSprite.Sprite.RelativeOrigin.X * scale, min.X); + max.X = Math.Max(worldPos.X + decorativeSprite.Sprite.size.X * (1.0f - decorativeSprite.Sprite.RelativeOrigin.X) * scale, max.X); + min.Y = Math.Min(worldPos.Y - decorativeSprite.Sprite.size.Y * (1.0f - decorativeSprite.Sprite.RelativeOrigin.Y) * scale, min.Y); + max.Y = Math.Max(worldPos.Y + decorativeSprite.Sprite.size.Y * decorativeSprite.Sprite.RelativeOrigin.Y * scale, max.Y); } if (min.X > worldView.Right || max.X < worldView.X) { return false; } @@ -220,9 +221,14 @@ namespace Barotrauma Draw(spriteBatch, editing, false, damageEffect); } + private float GetRealDepth() + { + return SpriteDepthOverrideIsSet ? SpriteOverrideDepth : prefab.sprite.Depth; + } + public float GetDrawDepth() { - return GetDrawDepth(SpriteDepthOverrideIsSet ? SpriteOverrideDepth : prefab.sprite.Depth, prefab.sprite); + return GetDrawDepth(GetRealDepth(), prefab.sprite); } private void Draw(SpriteBatch spriteBatch, bool editing, bool back = true, Effect damageEffect = null) @@ -320,7 +326,7 @@ namespace Barotrauma } } - if (back == depth > 0.5f) + if (back == GetRealDepth() > 0.5f) { SpriteEffects oldEffects = prefab.sprite.effects; prefab.sprite.effects ^= SpriteEffects; @@ -377,10 +383,10 @@ namespace Barotrauma foreach (var decorativeSprite in Prefab.DecorativeSprites) { if (!spriteAnimState[decorativeSprite].IsActive) { continue; } - float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState); - Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState) * Scale; + float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState, spriteAnimState[decorativeSprite].RandomRotationFactor); + Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier) * Scale; decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + offset.X, -(DrawPosition.Y + offset.Y)), color, - rotation, decorativeSprite.Scale * Scale, prefab.sprite.effects, + rotation, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, prefab.sprite.effects, depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - prefab.sprite.Depth), 0.999f)); } prefab.sprite.effects = oldEffects; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs index 6490192b3..20e6c3b99 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs @@ -62,7 +62,11 @@ namespace Barotrauma }; SubEditorScreen.StoreCommand(new AddOrDeleteCommand(new List { structure }, false)); - selected = null; + placePosition = Vector2.Zero; + if (!PlayerInput.IsShiftDown()) + { + selected = null; + } return; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs index 795fde68d..28e2bbdb2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs @@ -22,6 +22,8 @@ namespace Barotrauma public readonly float Range; public readonly Vector2 FrequencyMultiplierRange; public readonly bool Stream; + public readonly bool IgnoreMuffling; + public string Filename { @@ -55,7 +57,7 @@ namespace Barotrauma { DebugConsole.ThrowError($"Loaded frequency range exceeds max value: {FrequencyMultiplierRange} (original string was \"{freqMultAttr}\")"); } - sound.IgnoreMuffling = element.GetAttributeBool("dontmuffle", false); + IgnoreMuffling = element.GetAttributeBool("dontmuffle", false); } public float GetRandomFrequencyMultiplier() @@ -376,22 +378,19 @@ namespace Barotrauma public static void DrawGrid(SpriteBatch spriteBatch, int gridCells, Vector2 gridCenter, Vector2 roundedGridCenter, float alpha = 1.0f) { - var horizontalLine = GUI.Style.GetComponentStyle("HorizontalLine").GetDefaultSprite(); - var verticalLine = GUI.Style.GetComponentStyle("VerticalLine").GetDefaultSprite(); - Vector2 topLeft = roundedGridCenter - Vector2.One * GridSize * gridCells / 2; Vector2 bottomRight = roundedGridCenter + Vector2.One * GridSize * gridCells / 2; for (int i = 0; i < gridCells; i++) { float distFromGridX = (MathUtils.RoundTowardsClosest(gridCenter.X, GridSize.X) - gridCenter.X) / GridSize.X; - float distFromGridY = (MathUtils.RoundTowardsClosest(gridCenter.X, GridSize.Y) - gridCenter.X) / GridSize.Y; + float distFromGridY = (MathUtils.RoundTowardsClosest(gridCenter.Y, GridSize.Y) - gridCenter.Y) / GridSize.Y; float normalizedDistX = Math.Abs(i + distFromGridX - gridCells / 2) / (gridCells / 2); float normalizedDistY = Math.Abs(i - distFromGridY - gridCells / 2) / (gridCells / 2); - float expandX = MathHelper.Lerp(30.0f, 0.0f, normalizedDistX); - float expandY = MathHelper.Lerp(30.0f, 0.0f, normalizedDistY); + float expandX = MathHelper.Lerp(30.0f, 0.0f, normalizedDistY); + float expandY = MathHelper.Lerp(30.0f, 0.0f, normalizedDistX); GUI.DrawLine(spriteBatch, new Vector2(topLeft.X - expandX, -bottomRight.Y + i * GridSize.Y), @@ -420,9 +419,10 @@ namespace Barotrauma parent.RectTransform, Anchor.Center), style: null); + var connectedSubs = GetConnectedSubs(); foreach (Hull hull in Hull.hullList) { - if (hull.Submarine != this && !(DockedTo.Contains(hull.Submarine))) continue; + if (hull.Submarine != this && !connectedSubs.Contains(hull.Submarine)) { continue; } if (ignoreOutpost && !IsEntityFoundOnThisSub(hull, true)) { continue; } Vector2 relativeHullPos = new Vector2( @@ -533,11 +533,11 @@ namespace Barotrauma for (int i = 0; i < item.Connections.Count; i++) { int wireCount = item.Connections[i].Wires.Count(w => w != null); - if (doorLinks + wireCount > Connection.MaxLinked) + if (doorLinks + wireCount > item.Connections[i].MaxWires) { errorMsgs.Add(TextManager.GetWithVariables("InsufficientFreeConnectionsWarning", new string[] { "[doorcount]", "[freeconnectioncount]" }, - new string[] { doorLinks.ToString(), (Connection.MaxLinked - wireCount).ToString() })); + new string[] { doorLinks.ToString(), (item.Connections[i].MaxWires - wireCount).ToString() })); break; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs index 993d9f289..93b716257 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs @@ -10,6 +10,7 @@ namespace Barotrauma.Networking { public string Name; public string PreferredJob; + public CharacterTeamType PreferredTeam; public UInt16 NameID; public UInt64 SteamID; public byte ID; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index dc98b6ac2..0d9f1ab69 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -81,7 +81,7 @@ namespace Barotrauma.Networking private byte myID; - private List otherClients; + private readonly List otherClients; public readonly List ServerSubmarines = new List(); @@ -94,13 +94,13 @@ namespace Barotrauma.Networking private UInt16 lastSentChatMsgID = 0; //last message this client has successfully sent private UInt16 lastQueueChatMsgID = 0; //last message added to the queue - private List chatMsgQueue = new List(); + private readonly List chatMsgQueue = new List(); public UInt16 LastSentEntityEventID; - private ClientEntityEventManager entityEventManager; + private readonly ClientEntityEventManager entityEventManager; - private FileReceiver fileReceiver; + private readonly FileReceiver fileReceiver; #if DEBUG public void PrintReceiverTransters() @@ -138,6 +138,12 @@ namespace Barotrauma.Networking } } + private readonly List previouslyConnectedClients = new List(); + public IEnumerable PreviouslyConnectedClients + { + get { return previouslyConnectedClients; } + } + public FileReceiver FileReceiver { get { return fileReceiver; } @@ -153,9 +159,9 @@ namespace Barotrauma.Networking get { return entityEventManager; } } - private object serverEndpoint; - private int ownerKey; - private bool steamP2POwner; + private readonly object serverEndpoint; + private readonly int ownerKey; + private readonly bool steamP2POwner; public bool IsServerOwner { @@ -852,7 +858,7 @@ namespace Barotrauma.Networking endMessage = inc.ReadString(); bool missionSuccessful = inc.ReadBoolean(); - Character.TeamType winningTeam = (Character.TeamType)inc.ReadByte(); + CharacterTeamType winningTeam = (CharacterTeamType)inc.ReadByte(); if (missionSuccessful && GameMain.GameSession?.Mission != null) { GameMain.GameSession.WinningTeam = winningTeam; @@ -1634,7 +1640,7 @@ namespace Barotrauma.Networking { if (Submarine.MainSubs[i] == null) { break; } - var teamID = i == 0 ? Character.TeamType.Team1 : Character.TeamType.Team2; + var teamID = i == 0 ? CharacterTeamType.Team1 : CharacterTeamType.Team2; Submarine.MainSubs[i].TeamID = teamID; foreach (Item item in Item.ItemList) { @@ -1828,6 +1834,7 @@ namespace Barotrauma.Networking UInt16 nameId = inc.ReadUInt16(); string name = inc.ReadString(); string preferredJob = inc.ReadString(); + byte preferredTeam = inc.ReadByte(); UInt16 characterID = inc.ReadUInt16(); float karma = inc.ReadSingle(); bool muted = inc.ReadBoolean(); @@ -1844,6 +1851,7 @@ namespace Barotrauma.Networking SteamID = steamId, Name = name, PreferredJob = preferredJob, + PreferredTeam = (CharacterTeamType)preferredTeam, CharacterID = characterID, Karma = karma, Muted = muted, @@ -1878,6 +1886,7 @@ namespace Barotrauma.Networking } existingClient.NameID = tc.NameID; existingClient.PreferredJob = tc.PreferredJob; + existingClient.PreferredTeam = tc.PreferredTeam; existingClient.Character = null; existingClient.Karma = tc.Karma; existingClient.Muted = tc.Muted; @@ -1917,6 +1926,17 @@ namespace Barotrauma.Networking refreshCampaignUI = true; } } + foreach (Client client in ConnectedClients) + { + if (!previouslyConnectedClients.Any(c => c.ID == client.ID)) + { + while (previouslyConnectedClients.Count > 100) + { + previouslyConnectedClients.RemoveAt(0); + } + previouslyConnectedClients.Add(client); + } + } if (updateClientListId) { LastClientListUpdateID = listId; } if (clientPeer is SteamP2POwnerPeer) @@ -2142,6 +2162,7 @@ namespace Barotrauma.Networking } break; case ServerNetObject.ENTITY_POSITION: + bool isItem = inc.ReadBoolean(); UInt16 id = inc.ReadUInt16(); uint msgLength = inc.ReadVariableUInt32(); int msgEndPos = (int)(inc.BitPosition + msgLength * 8); @@ -2156,8 +2177,15 @@ namespace Barotrauma.Networking entities.Add(entity); if (entity != null && (entity is Item || entity is Character || entity is Submarine)) { - entity.ClientRead(objHeader.Value, inc, sendingTime); - } + if (entity is Item != isItem) + { + DebugConsole.AddWarning($"Received a potentially invalid ENTITY_POSITION message. Entity type does not match (server entity is {(isItem ? "an item" : "not an item")}, client entity is {(entity?.GetType().ToString() ?? "null")}). Ignoring the message..."); + } + else + { + entity.ClientRead(objHeader.Value, inc, sendingTime); + } + } //force to the correct position in case the entity doesn't exist //or the message wasn't read correctly for whatever reason @@ -2230,9 +2258,9 @@ namespace Barotrauma.Networking } GameAnalyticsManager.AddErrorEventOnce("GameClient.ReadInGameUpdate", GameAnalyticsSDK.Net.EGAErrorSeverity.Critical, string.Join("\n", errorLines)); - DebugConsole.ThrowError("Writing object data to \"crashreport_object.log\", please send this file to us at http://github.com/Regalis11/Barotrauma/issues"); + DebugConsole.ThrowError("Writing object data to \"networkerror_data.log\", please send this file to us at http://github.com/Regalis11/Barotrauma/issues"); - using (FileStream fl = File.Open("crashreport_object.log", System.IO.FileMode.Create)) + using (FileStream fl = File.Open("networkerror_data.log", System.IO.FileMode.Create)) { using (System.IO.BinaryWriter bw = new System.IO.BinaryWriter(fl)) using (System.IO.StreamWriter sw = new System.IO.StreamWriter(fl)) @@ -2245,7 +2273,7 @@ namespace Barotrauma.Networking } } } - throw new Exception("Read error: please send us \"crashreport_object.bin\"!"); + throw new Exception("Read error: please send us \"networkerror_data.log\"!"); } } @@ -2269,6 +2297,7 @@ namespace Barotrauma.Networking { outmsg.Write(""); } + outmsg.Write((byte)GameMain.Config.TeamPreference); if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaign.LastSaveID == 0) { @@ -3241,12 +3270,12 @@ namespace Barotrauma.Networking public virtual bool SelectCrewCharacter(Character character, GUIComponent frame) { - if (character == null) return false; + if (character == null) { return false; } if (character != myCharacter) { - var client = GameMain.NetworkMember.ConnectedClients.Find(c => c.Character == character); - if (client == null) return false; + var client = previouslyConnectedClients.Find(c => c.Character == character); + if (client == null) { return false; } CreateSelectionRelatedButtons(client, frame); } @@ -3256,7 +3285,7 @@ namespace Barotrauma.Networking public virtual bool SelectCrewClient(Client client, GUIComponent frame) { - if (client == null || client.ID == ID) return false; + if (client == null || client.ID == ID) { return false; } CreateSelectionRelatedButtons(client, frame); return true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/KarmaManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/KarmaManager.cs index 71b76babc..13a4dcebb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/KarmaManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/KarmaManager.cs @@ -38,6 +38,7 @@ namespace Barotrauma CreateLabeledSlider(parent, 0.0f, 1.0f, 0.01f, nameof(DamageEnemyKarmaIncrease)); CreateLabeledSlider(parent, 0.0f, 1.0f, 0.01f, nameof(ItemRepairKarmaIncrease)); CreateLabeledSlider(parent, 0.0f, 10.0f, 0.05f, nameof(ExtinguishFireKarmaIncrease)); + CreateLabeledSlider(parent, 0.0f, 1.0f, 0.01f, nameof(BallastFloraKarmaIncrease)); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.12f), parent.RectTransform), TextManager.Get("Karma.NegativeActions"), textAlignment: Alignment.Center, font: GUI.SubHeadingFont) @@ -61,7 +62,6 @@ namespace Barotrauma CreateLabeledNumberInput(parent, 0, 20, nameof(AllowedWireDisconnectionsPerMinute)); CreateLabeledSlider(parent, 0.0f, 20.0f, 0.5f, nameof(WireDisconnectionKarmaDecrease)); CreateLabeledSlider(parent, 0.0f, 30.0f, 1.0f, nameof(SpamFilterKarmaDecrease)); - CreateLabeledSlider(parent, 0.0f, 1.0f, 0.01f, nameof(BallastFloraKarmaIncrease)); //hide these for now if a localized text is not available if (TextManager.ContainsTag("Karma." + nameof(DangerousItemStealKarmaDecrease))) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs index 2a062c695..e0f4462ae 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs @@ -226,7 +226,7 @@ namespace Barotrauma.Networking } else { - long msgPosition = msg.BitPosition; + int msgPosition = msg.BitPosition; if (GameSettings.VerboseLogging) { DebugConsole.NewMessage("received msg " + thisEventID + " (" + entity.ToString() + ")", @@ -242,9 +242,9 @@ namespace Barotrauma.Networking { string errorMsg = "Message byte position incorrect after reading an event for the entity \"" + entity.ToString() + "\". Read " + (msg.BitPosition - msgPosition) + " bits, expected message length was " + (msgLength * 8) + " bits."; -#if DEBUG + DebugConsole.ThrowError(errorMsg); -#endif + GameAnalyticsManager.AddErrorEventOnce("ClientEntityEventManager.Read:BitPosMismatch", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); //TODO: force the BitPosition to correct place? Having some entity in a potentially incorrect state is not as bad as a desync kick diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs index c2b239942..3e1a48b58 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/ClientPeer.cs @@ -168,7 +168,19 @@ namespace Barotrauma.Networking { Close(disableReconnect: true); - string missingModNames = "\n- " + string.Join("\n\n- ", missingPackages.Select(p => GetPackageStr(p))) + "\n\n"; + string missingModNames = "\n"; + int displayedModCount = 0; + foreach (ServerContentPackage missingPackage in missingPackages) + { + missingModNames += "\n- " + GetPackageStr(missingPackage); + displayedModCount++; + if (GUI.Font.MeasureString(missingModNames).Y > GameMain.GraphicsHeight * 0.5f) + { + missingModNames += "\n\n" + TextManager.GetWithVariable("workshopitemdownloadprompttruncated", "[number]", (missingPackages.Count - displayedModCount).ToString()); + break; + } + } + missingModNames += "\n\n"; var msgBox = new GUIMessageBox( TextManager.Get("WorkshopItemDownloadTitle"), @@ -189,6 +201,7 @@ namespace Barotrauma.Networking if (!contentPackageOrderReceived) { + GameMain.Config.BackUpModOrder(); GameMain.Config.SwapPackages(corePackage.CorePackage, regularPackages.Select(p => p.RegularPackage).ToList()); contentPackageOrderReceived = true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerLog.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerLog.cs index 0e1f02597..b4e942b20 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerLog.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerLog.cs @@ -195,10 +195,11 @@ namespace Barotrauma.Networking { foreach (var data in line.RichData) { - UInt64 id = 0; - if (!UInt64.TryParse(data.Metadata, out id)) { return; } - Client client = GameMain.Client.ConnectedClients.Find(c => c.SteamID == id); - client ??= GameMain.Client.ConnectedClients.Find(c => c.ID == id); + if (!UInt64.TryParse(data.Metadata, out ulong id)) { return; } + Client client = GameMain.Client.ConnectedClients.Find(c => c.SteamID == id) + ?? GameMain.Client.ConnectedClients.Find(c => c.ID == id) + ?? GameMain.Client.PreviouslyConnectedClients.FirstOrDefault(c => c.SteamID == id) + ?? GameMain.Client.PreviouslyConnectedClients.FirstOrDefault(c => c.ID == id); if (client != null && client.Karma < 40.0f) { textContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.0f), listBox.Content.RectTransform), @@ -243,10 +244,11 @@ namespace Barotrauma.Networking Data = data, OnClick = (component, area) => { - UInt64 id = 0; - if (!UInt64.TryParse(area.Data.Metadata, out id)) { return; } - Client client = GameMain.Client.ConnectedClients.Find(c => c.SteamID == id); - client ??= GameMain.Client.ConnectedClients.Find(c => c.ID == id); + if (!UInt64.TryParse(area.Data.Metadata, out UInt64 id)) { return; } + Client client = GameMain.Client.ConnectedClients.Find(c => c.SteamID == id) + ?? GameMain.Client.ConnectedClients.Find(c => c.ID == id) + ?? GameMain.Client.PreviouslyConnectedClients.FirstOrDefault(c => c.SteamID == id) + ?? GameMain.Client.PreviouslyConnectedClients.FirstOrDefault(c => c.ID == id); if (client == null) { return; } GameMain.NetLobbyScreen.SelectPlayer(client); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs index 21e1faa8a..90e5bb010 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs @@ -269,14 +269,13 @@ namespace Barotrauma.Steam } } - //TODO: find a better strategy to fetch all lobbies, this is gonna take forever if we actually have 10000 lobbies - Steamworks.Data.LobbyQuery lobbyQuery = Steamworks.SteamMatchmaking.CreateLobbyQuery().FilterDistanceWorldwide().WithMaxResults(10000); Steamworks.Dispatch.OnDebugCallback = (callbackType, contents, isServer) => { DebugConsole.NewMessage($"{callbackType}: " + contents, Color.Yellow); }; - TaskPool.Add("LobbyQueryRequest", lobbyQuery.RequestAsync(), + + TaskPool.Add("LobbyQueryRequest", LobbyQueryRequest(), (t) => { Steamworks.Dispatch.OnDebugCallback = null; @@ -286,7 +285,7 @@ namespace Barotrauma.Steam taskDone(); return; } - var lobbies = ((Task)t).Result; + var lobbies = ((Task>)t).Result; if (lobbies != null) { foreach (var lobby in lobbies) @@ -373,6 +372,32 @@ namespace Barotrauma.Steam return true; } + public static async Task> LobbyQueryRequest() + { + List allLobbies = new List(); + Steamworks.Data.LobbyQuery lobbyQuery = Steamworks.SteamMatchmaking.CreateLobbyQuery() + .FilterDistanceWorldwide() + .WithMaxResults(50); + //steamworks seems to unable to retrieve more than 50 + //lobbies per request; to work around this, we'll make + //up to 10 requests, asking to ignore all previous results + //in each subsequent request + for (int i = 0; i < 10; i++) + { + Steamworks.Data.Lobby[] lobbies = await lobbyQuery.RequestAsync(); + if (lobbies == null) { break; } + foreach (var l in lobbies) + { + lobbyQuery = lobbyQuery + .WithoutKeyValue("lobbyowner", l.GetData("lobbyowner")); + } + allLobbies.AddRange(lobbies); + } + + //make sure all returned lobbies are distinct, don't want any duplicates here + return allLobbies.Select(l => l.Id).Distinct().Select(i => allLobbies.Find(l => l.Id == i)).ToList(); + } + public static void AssignLobbyDataToServerInfo(Steamworks.Data.Lobby lobby, ServerInfo serverInfo) { serverInfo.OwnerVerified = true; @@ -1173,7 +1198,7 @@ namespace Barotrauma.Steam foreach (ContentFile contentFile in contentPackage.Files) { - contentFile.Path = contentFile.Path.CleanUpPath(); + contentFile.Path = contentFile.Path.CleanUpPathCrossPlatform(correctFilenameCase: true, item?.Directory); string sourceFile = Path.Combine(item?.Directory, contentFile.Path); if (!File.Exists(sourceFile)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs index f9724ead2..be3b37eea 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs @@ -239,7 +239,7 @@ namespace Barotrauma.Networking bool allowEnqueue = false; if (GameMain.WindowActive) { - ForceLocal = captureTimer > 0 ? ForceLocal : false; + ForceLocal = captureTimer > 0 ? ForceLocal : GameMain.Config.UseLocalVoiceByDefault; bool pttDown = false; if ((PlayerInput.KeyDown(InputType.Voice) || PlayerInput.KeyDown(InputType.LocalVoice)) && GUI.KeyboardDispatcher.Subscriber == null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs index b648cde9c..b4fd76f45 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs @@ -101,7 +101,7 @@ namespace Barotrauma.Networking var messageType = !client.VoipQueue.ForceLocal && ChatMessage.CanUseRadio(client.Character, out radio) ? ChatMessageType.Radio : ChatMessageType.Default; client.Character.ShowSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]); - client.VoipSound.UseRadioFilter = messageType == ChatMessageType.Radio; + client.VoipSound.UseRadioFilter = messageType == ChatMessageType.Radio && !GameMain.Config.DisableVoiceChatFilters; if (client.VoipSound.UseRadioFilter) { client.VoipSound.SetRange(radio.Range * 0.8f, radio.Range); @@ -110,7 +110,7 @@ namespace Barotrauma.Networking { client.VoipSound.SetRange(ChatMessage.SpeakRange * 0.4f, ChatMessage.SpeakRange); } - if (!client.VoipSound.UseRadioFilter && Character.Controlled != null) + if (!client.VoipSound.UseRadioFilter && Character.Controlled != null && !GameMain.Config.DisableVoiceChatFilters) { client.VoipSound.UseMuffleFilter = SoundPlayer.ShouldMuffleSound(Character.Controlled, client.Character.WorldPosition, ChatMessage.SpeakRange, client.Character.CurrentHull); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI.cs index 89fb295b4..7602d3873 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI.cs @@ -11,15 +11,15 @@ namespace Barotrauma { class CampaignSetupUI { - private GUIComponent newGameContainer, loadGameContainer; + private readonly GUIComponent newGameContainer, loadGameContainer; private GUIListBox subList; private GUIListBox saveList; private List subTickBoxes; - private GUITextBox saveNameBox, seedBox; + private readonly GUITextBox saveNameBox, seedBox; - private GUILayoutGroup subPreviewContainer; + private readonly GUILayoutGroup subPreviewContainer; private GUIButton loadGameButton, deleteMpSaveButton; @@ -35,6 +35,12 @@ namespace Barotrauma private set; } + public GUITextBlock InitialMoneyText + { + get; + private set; + } + private readonly bool isMultiplayer; public CampaignSetupUI(bool isMultiplayer, GUIComponent newGameContainer, GUIComponent loadGameContainer, IEnumerable submarines, IEnumerable saveFiles = null) @@ -122,10 +128,10 @@ namespace Barotrauma }; var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.12f), - (isMultiplayer ? leftColumn : rightColumn).RectTransform) { MaxSize = new Point(int.MaxValue, 60) }, childAnchor: Anchor.TopRight); + (isMultiplayer ? leftColumn : rightColumn).RectTransform) { MaxSize = new Point(int.MaxValue, 60) }, childAnchor: Anchor.BottomRight, isHorizontal: true); if (!isMultiplayer) { buttonContainer.IgnoreLayoutGroups = true; } - StartButton = new GUIButton(new RectTransform(new Vector2(0.45f, 1f), buttonContainer.RectTransform, Anchor.BottomRight) { MaxSize = new Point(350, 60) }, TextManager.Get("StartCampaignButton")) + StartButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1f), buttonContainer.RectTransform, Anchor.BottomRight) { MaxSize = new Point(350, 60) }, TextManager.Get("StartCampaignButton")) { OnClicked = (GUIButton btn, object userData) => { @@ -224,6 +230,27 @@ namespace Barotrauma } }; + InitialMoneyText = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1f), buttonContainer.RectTransform), "", + font: isMultiplayer ? GUI.Style.SmallFont : GUI.Style.Font, textColor: GUI.Style.Green) + { + TextGetter = () => + { + int initialMoney = CampaignMode.InitialMoney; + if (isMultiplayer) + { + if (GameMain.NetLobbyScreen.SelectedSub != null) + { + initialMoney -= GameMain.NetLobbyScreen.SelectedSub.Price; + } + } + else if (subList.SelectedData is SubmarineInfo subInfo) + { + initialMoney -= subInfo.Price; + } + initialMoney = Math.Max(initialMoney, isMultiplayer ? MultiPlayerCampaign.MinimumInitialMoney : 0); + return TextManager.GetWithVariable("campaignstartingmoney", "[money]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", initialMoney)); + } + }; if (!isMultiplayer) { @@ -366,7 +393,7 @@ namespace Barotrauma if (!(obj is SubmarineInfo sub)) { return true; } #if !DEBUG - if (!isMultiplayer && sub.Price > CampaignMode.MaxInitialSubmarinePrice && !GameMain.DebugDraw) + if (!isMultiplayer && sub.Price > CampaignMode.InitialMoney && !GameMain.DebugDraw) { StartButton.Enabled = false; return false; @@ -419,13 +446,14 @@ namespace Barotrauma } else { - subsToShow = submarines.Where(s => s.IsCampaignCompatibleIgnoreClass).ToList(); + string downloadFolder = Path.GetFullPath(SaveUtil.SubmarineDownloadFolder); + subsToShow = submarines.Where(s => s.IsCampaignCompatibleIgnoreClass && Path.GetDirectoryName(Path.GetFullPath(s.FilePath)) != downloadFolder).ToList(); } subsToShow.Sort((s1, s2) => { - int p1 = s1.Price > CampaignMode.MaxInitialSubmarinePrice ? 10 : 0; - int p2 = s2.Price > CampaignMode.MaxInitialSubmarinePrice ? 10 : 0; + int p1 = s1.Price > CampaignMode.InitialMoney ? 10 : 0; + int p2 = s2.Price > CampaignMode.InitialMoney ? 10 : 0; return p1.CompareTo(p2) * 100 + s1.Name.CompareTo(s2.Name); }); @@ -450,13 +478,13 @@ namespace Barotrauma var priceText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), textBlock.RectTransform, Anchor.CenterRight), TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", sub.Price)), textAlignment: Alignment.CenterRight, font: GUI.SmallFont) { - TextColor = sub.Price > CampaignMode.MaxInitialSubmarinePrice ? GUI.Style.Red : textBlock.TextColor * 0.8f, + TextColor = sub.Price > CampaignMode.InitialMoney ? GUI.Style.Red : textBlock.TextColor * 0.8f, ToolTip = textBlock.ToolTip }; #if !DEBUG if (!GameMain.DebugDraw) { - if (sub.Price > CampaignMode.MaxInitialSubmarinePrice || !sub.IsCampaignCompatible) + if (sub.Price > CampaignMode.InitialMoney || !sub.IsCampaignCompatible) { textBlock.CanBeFocused = false; textBlock.TextColor *= 0.5f; @@ -466,7 +494,7 @@ namespace Barotrauma } if (SubmarineInfo.SavedSubmarines.Any()) { - var validSubs = subsToShow.Where(s => s.IsCampaignCompatible && s.Price <= CampaignMode.MaxInitialSubmarinePrice).ToList(); + var validSubs = subsToShow.Where(s => s.IsCampaignCompatible && s.Price <= CampaignMode.InitialMoney).ToList(); if (validSubs.Count > 0) { subList.Select(validSubs[Rand.Int(validSubs.Count)]); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs index 43370a74b..7f72b71d3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs @@ -326,7 +326,7 @@ namespace Barotrauma break; case CampaignMode.InteractionType.Store: - Store?.Update(); + Store?.Update(deltaTime); break; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index eac9ab768..d0aba1404 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -820,6 +820,7 @@ namespace Barotrauma.CharacterEditor } } } + spriteBatch.End(); // Lights @@ -860,10 +861,7 @@ namespace Barotrauma.CharacterEditor } if (isDrawingLimb) { - if (spriteSheetRect.Contains(PlayerInput.MousePosition)) - { - GUI.DrawRectangle(spriteBatch, newLimbRect, Color.Yellow); - } + GUI.DrawRectangle(spriteBatch, newLimbRect, Color.Yellow); } if (jointCreationMode != JointCreationMode.None) { @@ -1131,32 +1129,25 @@ namespace Barotrauma.CharacterEditor { SetToggle(limbsToggle, true); } - if (spriteSheetRect.Contains(PlayerInput.MousePosition)) + if (PlayerInput.PrimaryMouseButtonHeld()) { - if (PlayerInput.PrimaryMouseButtonHeld()) + if (newLimbRect == Rectangle.Empty) { - if (newLimbRect == Rectangle.Empty) - { - newLimbRect = new Rectangle((int)PlayerInput.MousePosition.X, (int)PlayerInput.MousePosition.Y, 0, 0); - } - else - { - newLimbRect.Size = new Point((int)PlayerInput.MousePosition.X - newLimbRect.X, (int)PlayerInput.MousePosition.Y - newLimbRect.Y); - } - newLimbRect.Size = new Point(Math.Max(newLimbRect.Width, 2), Math.Max(newLimbRect.Height, 2)); + newLimbRect = new Rectangle((int)PlayerInput.MousePosition.X, (int)PlayerInput.MousePosition.Y, 0, 0); } - if (PlayerInput.PrimaryMouseButtonClicked()) + else { - // Take the offset and the zoom into account - newLimbRect.Location = new Point(newLimbRect.X - spriteSheetOffsetX, newLimbRect.Y - spriteSheetOffsetY); - newLimbRect = newLimbRect.Divide(spriteSheetZoom); - CreateNewLimb(newLimbRect); - isDrawingLimb = false; - newLimbRect = Rectangle.Empty; + newLimbRect.Size = new Point((int)PlayerInput.MousePosition.X - newLimbRect.X, (int)PlayerInput.MousePosition.Y - newLimbRect.Y); } + newLimbRect.Size = new Point(Math.Max(newLimbRect.Width, 2), Math.Max(newLimbRect.Height, 2)); } - else + if (PlayerInput.PrimaryMouseButtonClicked()) { + // Take the offset and the zoom into account + newLimbRect.Location = new Point(newLimbRect.X - spriteSheetOffsetX, newLimbRect.Y - spriteSheetOffsetY); + newLimbRect = newLimbRect.Divide(spriteSheetZoom); + CreateNewLimb(newLimbRect); + isDrawingLimb = false; newLimbRect = Rectangle.Empty; } } @@ -1449,7 +1440,11 @@ namespace Barotrauma.CharacterEditor { if (allFiles == null) { +#if DEBUG allFiles = CharacterPrefab.ConfigFilePaths.OrderBy(p => p).ToList(); +#else + allFiles = CharacterPrefab.ConfigFilePaths.Where(p => !p.Contains("variant", StringComparison.OrdinalIgnoreCase)).OrderBy(p => p).ToList(); +#endif allFiles.ForEach(f => DebugConsole.NewMessage(f, Color.White)); } return allFiles; @@ -1780,7 +1775,7 @@ namespace Barotrauma.CharacterEditor // Animations AnimationParams.ClearCache(); - string animFolder = AnimationParams.GetFolder(name, contentPackage); + string animFolder = AnimationParams.GetFolder(name); if (animations != null) { if (!Directory.Exists(animFolder)) @@ -1791,7 +1786,7 @@ namespace Barotrauma.CharacterEditor { XElement element = animation.MainElement; element.SetAttributeValue("type", name); - string fullPath = AnimationParams.GetDefaultFile(name, animation.AnimationType, contentPackage); + string fullPath = AnimationParams.GetDefaultFile(name, animation.AnimationType); element.Name = AnimationParams.GetDefaultFileName(name, animation.AnimationType); #if DEBUG element.Save(fullPath); @@ -1816,7 +1811,7 @@ namespace Barotrauma.CharacterEditor default: continue; } Type type = AnimationParams.GetParamTypeFromAnimType(animType, isHumanoid); - string fullPath = AnimationParams.GetDefaultFile(name, animType, contentPackage); + string fullPath = AnimationParams.GetDefaultFile(name, animType); AnimationParams.Create(fullPath, name, animType, type); } } @@ -1836,9 +1831,8 @@ namespace Barotrauma.CharacterEditor private void ShowWearables() { if (character.Inventory == null) { return; } - foreach (var item in character.Inventory.Items) + foreach (var item in character.Inventory.AllItems) { - if (item == null) { continue; } // Temp condition, todo: remove if (item.AllowedSlots.Contains(InvSlotType.Head) || item.AllowedSlots.Contains(InvSlotType.Headset)) { continue; } item.Equip(character); @@ -1847,7 +1841,7 @@ namespace Barotrauma.CharacterEditor private void HideWearables() { - character.Inventory?.Items.ForEachMod(i => i?.Unequip(character)); + character.Inventory?.AllItemsMod.ForEach(i => i.Unequip(character)); } #endregion @@ -2787,8 +2781,10 @@ namespace Barotrauma.CharacterEditor } return true; }; + // Spacing new GUIFrame(new RectTransform(buttonSize / 2, layoutGroup.RectTransform), style: null) { CanBeFocused = false }; + Vector2 messageBoxRelSize = new Vector2(0.5f, 0.7f); var saveRagdollButton = new GUIButton(new RectTransform(buttonSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("SaveRagdoll")); saveRagdollButton.OnClicked += (button, userData) => @@ -3573,15 +3569,33 @@ namespace Barotrauma.CharacterEditor return rect; } - // TODO: refactor this so that it can be used in all cases - private void UpdateSourceRect(Limb limb, Rectangle newRect) + private void UpdateSourceRect(Limb limb, Rectangle newRect, bool resize) { - limb.ActiveSprite.SourceRect = newRect; + Sprite activeSprite = limb.ActiveSprite; + activeSprite.SourceRect = newRect; if (limb.DamagedSprite != null) { - limb.DamagedSprite.SourceRect = limb.ActiveSprite.SourceRect; + limb.DamagedSprite.SourceRect = activeSprite.SourceRect; + } + Vector2 colliderSize = new Vector2(ConvertUnits.ToSimUnits(newRect.Width), ConvertUnits.ToSimUnits(newRect.Height)); + if (resize) + { + if (recalculateCollider) + { + RecalculateCollider(limb, colliderSize); + } + } + var spritePos = new Vector2(spriteSheetOffsetX, GetOffsetY(activeSprite)); + var originWidget = GetLimbEditWidget($"{limb.Params.ID}_origin", limb); + if (!resize && originWidget != null) + { + Vector2 newOrigin = (originWidget.DrawPos - spritePos - activeSprite.SourceRect.Location.ToVector2() * spriteSheetZoom) / spriteSheetZoom; + RecalculateOrigin(limb, newOrigin); + } + else + { + RecalculateOrigin(limb); } - RecalculateOrigin(limb); TryUpdateLimbParam(limb, "sourcerect", newRect); if (limbPairEditing) { @@ -3592,30 +3606,25 @@ namespace Barotrauma.CharacterEditor { otherLimb.DamagedSprite.SourceRect = newRect; } + if (resize) + { + if (recalculateCollider) + { + RecalculateCollider(otherLimb, colliderSize); + } + } + if (!resize && originWidget != null) + { + Vector2 newOrigin = (originWidget.DrawPos - spritePos - activeSprite.SourceRect.Location.ToVector2() * spriteSheetZoom) / spriteSheetZoom; + RecalculateOrigin(otherLimb, newOrigin); + } + else + { + RecalculateOrigin(otherLimb); + } TryUpdateLimbParam(otherLimb, "sourcerect", newRect); - RecalculateOrigin(otherLimb); }); }; - - void RecalculateOrigin(Limb l) - { - // Keeps the relative origin unchanged. The absolute origin will be recalculated. - l.ActiveSprite.RelativeOrigin = l.ActiveSprite.RelativeOrigin; - - // TODO: - //if (lockSpriteOrigin) - //{ - // // Keeps the absolute origin unchanged. The relative origin will be recalculated. - // var spritePos = new Vector2(spriteSheetOffsetX, GetOffsetY(l)); - // l.ActiveSprite.Origin = (originWidget.DrawPos - spritePos - l.ActiveSprite.SourceRect.Location.ToVector2() * spriteSheetZoom) / spriteSheetZoom; - // TryUpdateLimbParam(l, "origin", l.ActiveSprite.RelativeOrigin); - //} - //else - //{ - // // Keeps the relative origin unchanged. The absolute origin will be recalculated. - // l.ActiveSprite.RelativeOrigin = l.ActiveSprite.RelativeOrigin; - //} - } } private void CalculateSpritesheetZoom() @@ -4765,7 +4774,7 @@ namespace Barotrauma.CharacterEditor w.refresh(); w.MouseHeld += dTime => { - var spritePos = new Vector2(spriteSheetOffsetX, GetOffsetY(limb)); + var spritePos = new Vector2(spriteSheetOffsetX, GetOffsetY(limb.ActiveSprite)); w.DrawPos = PlayerInput.MousePosition.Clamp(spritePos + GetTopLeft() * spriteSheetZoom, spritePos + GetBottomRight() * spriteSheetZoom); sprite.Origin = (w.DrawPos - spritePos - sprite.SourceRect.Location.ToVector2() * spriteSheetZoom) / spriteSheetZoom; if (limb.DamagedSprite != null) @@ -4796,14 +4805,14 @@ namespace Barotrauma.CharacterEditor }; w.PreDraw += (sb, dTime) => { - var spritePos = new Vector2(spriteSheetOffsetX, GetOffsetY(limb)); + var spritePos = new Vector2(spriteSheetOffsetX, GetOffsetY(limb.ActiveSprite)); w.DrawPos = (spritePos + (sprite.Origin + sprite.SourceRect.Location.ToVector2()) * spriteSheetZoom) .Clamp(spritePos + GetTopLeft() * spriteSheetZoom, spritePos + GetBottomRight() * spriteSheetZoom); w.refresh(); }; }); originWidget.Draw(spriteBatch, deltaTime); - if (!lockSpritePosition) + if (!lockSpritePosition && (limb.type != LimbType.Head || !character.IsHuman)) { var positionWidget = GetLimbEditWidget($"{limb.Params.ID}_position", limb, widgetSize, Widget.Shape.Rectangle, initMethod: w => { @@ -4812,17 +4821,20 @@ namespace Barotrauma.CharacterEditor w.MouseHeld += dTime => { w.DrawPos = PlayerInput.MousePosition; - var newRect = limb.ActiveSprite.SourceRect; + Sprite activeSprite = limb.ActiveSprite; + var newRect = activeSprite.SourceRect; newRect.Location = new Point( (int)((PlayerInput.MousePosition.X + halfSize - spriteSheetOffsetX) / spriteSheetZoom), - (int)((PlayerInput.MousePosition.Y + halfSize - GetOffsetY(limb)) / spriteSheetZoom)); - limb.ActiveSprite.SourceRect = newRect; + (int)((PlayerInput.MousePosition.Y + halfSize - GetOffsetY(activeSprite)) / spriteSheetZoom)); + activeSprite.SourceRect = newRect; if (limb.DamagedSprite != null) { - limb.DamagedSprite.SourceRect = limb.ActiveSprite.SourceRect; + limb.DamagedSprite.SourceRect = activeSprite.SourceRect; } - RecalculateOrigin(limb); TryUpdateLimbParam(limb, "sourcerect", newRect); + var spritePos = new Vector2(spriteSheetOffsetX, GetOffsetY(activeSprite)); + Vector2 newOrigin = (originWidget.DrawPos - spritePos - activeSprite.SourceRect.Location.ToVector2() * spriteSheetZoom) / spriteSheetZoom; + RecalculateOrigin(limb, newOrigin); if (limbPairEditing) { UpdateOtherLimbs(limb, otherLimb => @@ -4833,24 +4845,9 @@ namespace Barotrauma.CharacterEditor otherLimb.DamagedSprite.SourceRect = newRect; } TryUpdateLimbParam(otherLimb, "sourcerect", newRect); - RecalculateOrigin(otherLimb); + RecalculateOrigin(otherLimb, newOrigin); }); }; - void RecalculateOrigin(Limb l) - { - if (lockSpriteOrigin) - { - // Keeps the absolute origin unchanged. The relative origin will be recalculated. - var spritePos = new Vector2(spriteSheetOffsetX, GetOffsetY(l)); - l.ActiveSprite.Origin = (originWidget.DrawPos - spritePos - l.ActiveSprite.SourceRect.Location.ToVector2() * spriteSheetZoom) / spriteSheetZoom; - TryUpdateLimbParam(l, "origin", l.ActiveSprite.RelativeOrigin); - } - else - { - // Keeps the relative origin unchanged. The absolute origin will be recalculated. - l.ActiveSprite.RelativeOrigin = l.ActiveSprite.RelativeOrigin; - } - } }; w.PreDraw += (sb, dTime) => w.refresh(); }); @@ -4860,7 +4857,7 @@ namespace Barotrauma.CharacterEditor } positionWidget.Draw(spriteBatch, deltaTime); } - if (!lockSpriteSize) + if (!lockSpriteSize && (limb.type != LimbType.Head || !character.IsHuman)) { var sizeWidget = GetLimbEditWidget($"{limb.Params.ID}_size", limb, widgetSize, Widget.Shape.Rectangle, initMethod: w => { @@ -4869,22 +4866,24 @@ namespace Barotrauma.CharacterEditor w.MouseHeld += dTime => { w.DrawPos = PlayerInput.MousePosition; - var newRect = limb.ActiveSprite.SourceRect; - float offset_y = limb.ActiveSprite.SourceRect.Y * spriteSheetZoom + GetOffsetY(limb); - float offset_x = limb.ActiveSprite.SourceRect.X * spriteSheetZoom + spriteSheetOffsetX; + Sprite activeSprite = limb.ActiveSprite; + Rectangle newRect = activeSprite.SourceRect; + float offset_y = activeSprite.SourceRect.Y * spriteSheetZoom + GetOffsetY(activeSprite); + float offset_x = activeSprite.SourceRect.X * spriteSheetZoom + spriteSheetOffsetX; int width = (int)((PlayerInput.MousePosition.X - halfSize - offset_x) / spriteSheetZoom); int height = (int)((PlayerInput.MousePosition.Y - halfSize - offset_y) / spriteSheetZoom); newRect.Size = new Point(width, height); - limb.ActiveSprite.SourceRect = newRect; - limb.ActiveSprite.size = new Vector2(width, height); + activeSprite.SourceRect = newRect; + activeSprite.size = new Vector2(width, height); + Vector2 colliderSize = new Vector2(ConvertUnits.ToSimUnits(width), ConvertUnits.ToSimUnits(height)); if (recalculateCollider) { - RecalculateCollider(limb); + RecalculateCollider(limb, colliderSize); } RecalculateOrigin(limb); if (limb.DamagedSprite != null) { - limb.DamagedSprite.SourceRect = limb.ActiveSprite.SourceRect; + limb.DamagedSprite.SourceRect = activeSprite.SourceRect; } TryUpdateLimbParam(limb, "sourcerect", newRect); if (limbPairEditing) @@ -4895,7 +4894,7 @@ namespace Barotrauma.CharacterEditor RecalculateOrigin(otherLimb); if (recalculateCollider) { - RecalculateCollider(otherLimb); + RecalculateCollider(otherLimb, colliderSize); } if (otherLimb.DamagedSprite != null) { @@ -4904,29 +4903,6 @@ namespace Barotrauma.CharacterEditor TryUpdateLimbParam(otherLimb, "sourcerect", newRect); }); }; - void RecalculateCollider(Limb l) - { - // We want the collider to be slightly smaller than the source rect, because the source rect is usually a bit bigger than the graphic. - float multiplier = 0.9f; - l.body.SetSize(new Vector2(ConvertUnits.ToSimUnits(width), ConvertUnits.ToSimUnits(height)) * l.Scale * RagdollParams.TextureScale * multiplier); - TryUpdateLimbParam(l, "radius", ConvertUnits.ToDisplayUnits(l.body.radius / l.Params.Scale / RagdollParams.LimbScale / RagdollParams.TextureScale)); - TryUpdateLimbParam(l, "width", ConvertUnits.ToDisplayUnits(l.body.width / l.Params.Scale / RagdollParams.LimbScale / RagdollParams.TextureScale)); - TryUpdateLimbParam(l, "height", ConvertUnits.ToDisplayUnits(l.body.height / l.Params.Scale / RagdollParams.LimbScale / RagdollParams.TextureScale)); - } - void RecalculateOrigin(Limb l) - { - if (lockSpriteOrigin) - { - // Keeps the absolute origin unchanged. The relative origin will be recalculated. - l.ActiveSprite.Origin = l.ActiveSprite.Origin; - TryUpdateLimbParam(l, "origin", l.ActiveSprite.RelativeOrigin); - } - else - { - // Keeps the relative origin unchanged. The absolute origin will be recalculated. - l.ActiveSprite.RelativeOrigin = l.ActiveSprite.RelativeOrigin; - } - } }; w.PreDraw += (sb, dTime) => w.refresh(); }); @@ -4955,22 +4931,48 @@ namespace Barotrauma.CharacterEditor } offsetY += (int)(texture.Height * spriteSheetZoom); } + } - int GetTextureHeight(Limb limb) + private int GetTextureHeight(Sprite sprite) + { + int textureIndex = Textures.IndexOf(sprite.Texture); + int height = 0; + foreach (var t in Textures) { - int textureIndex = Textures.IndexOf(limb.ActiveSprite.Texture); - int height = 0; - foreach (var t in Textures) + if (Textures.IndexOf(t) < textureIndex) { - if (Textures.IndexOf(t) < textureIndex) - { - height += t.Height; - } + height += t.Height; } - return (int)(height * spriteSheetZoom); } + return (int)(height * spriteSheetZoom); + } - int GetOffsetY(Limb limb) => spriteSheetOffsetY + GetTextureHeight(limb); + private int GetOffsetY(Sprite sprite) => spriteSheetOffsetY + GetTextureHeight(sprite); + + private void RecalculateCollider(Limb l, Vector2 size) + { + // We want the collider to be slightly smaller than the source rect, because the source rect is usually a bit bigger than the graphic. + float multiplier = 0.9f; + l.body.SetSize(new Vector2(size.X, size.Y) * l.Scale * RagdollParams.TextureScale * multiplier); + TryUpdateLimbParam(l, "radius", ConvertUnits.ToDisplayUnits(l.body.radius / l.Params.Scale / RagdollParams.LimbScale / RagdollParams.TextureScale)); + TryUpdateLimbParam(l, "width", ConvertUnits.ToDisplayUnits(l.body.width / l.Params.Scale / RagdollParams.LimbScale / RagdollParams.TextureScale)); + TryUpdateLimbParam(l, "height", ConvertUnits.ToDisplayUnits(l.body.height / l.Params.Scale / RagdollParams.LimbScale / RagdollParams.TextureScale)); + } + + private void RecalculateOrigin(Limb l, Vector2? newOrigin = null) + { + Sprite activeSprite = l.ActiveSprite; + if (lockSpriteOrigin) + { + // Keeps the absolute origin unchanged. The relative origin will be recalculated. + activeSprite.Origin = newOrigin ?? activeSprite.Origin; + TryUpdateLimbParam(l, "origin", activeSprite.RelativeOrigin); + } + else + { + // Keeps the relative origin unchanged. The absolute origin will be recalculated. + activeSprite.RelativeOrigin = activeSprite.RelativeOrigin; + } } private void DrawSpritesheetJointEditor(SpriteBatch spriteBatch, float deltaTime, Limb limb, Vector2 limbScreenPos, float spriteRotation = 0) @@ -5175,61 +5177,81 @@ namespace Barotrauma.CharacterEditor case Keys.Left: foreach (var limb in selectedLimbs) { + // Can't edit human heads + if (limb.type == LimbType.Head && character.IsHuman) { continue; } var newRect = limb.ActiveSprite.SourceRect; - if (PlayerInput.KeyDown(Keys.LeftControl)) + bool resize = PlayerInput.KeyDown(Keys.LeftControl); + if (resize) { + if (lockSpriteSize) { return; } newRect.Width--; } else { + if (lockSpritePosition) { return; } newRect.X--; } - UpdateSourceRect(limb, newRect); + UpdateSourceRect(limb, newRect, resize); } break; case Keys.Right: foreach (var limb in selectedLimbs) { + // Can't edit human heads + if (limb.type == LimbType.Head && character.IsHuman) { continue; } var newRect = limb.ActiveSprite.SourceRect; - if (PlayerInput.KeyDown(Keys.LeftControl)) + bool resize = PlayerInput.KeyDown(Keys.LeftControl); + if (resize) { + if (lockSpriteSize) { return; } newRect.Width++; } else { + if (lockSpritePosition) { return; } newRect.X++; } - UpdateSourceRect(limb, newRect); + UpdateSourceRect(limb, newRect, resize); } break; case Keys.Down: foreach (var limb in selectedLimbs) { + // Can't edit human heads + if (limb.type == LimbType.Head && character.IsHuman) { continue; } var newRect = limb.ActiveSprite.SourceRect; - if (PlayerInput.KeyDown(Keys.LeftControl)) + bool resize = PlayerInput.KeyDown(Keys.LeftControl); + if (resize) { + if (lockSpriteSize) { return; } newRect.Height++; } else { + if (lockSpritePosition) { return; } newRect.Y++; } - UpdateSourceRect(limb, newRect); + UpdateSourceRect(limb, newRect, resize); } break; case Keys.Up: foreach (var limb in selectedLimbs) { + // Can't edit human heads + if (limb.type == LimbType.Head && character.IsHuman) { continue; } var newRect = limb.ActiveSprite.SourceRect; - if (PlayerInput.KeyDown(Keys.LeftControl)) + bool resize = PlayerInput.KeyDown(Keys.LeftControl); + if (resize) { + if (lockSpriteSize) { return; } newRect.Height--; } else { + if (lockSpritePosition) { return; } newRect.Y--; } - UpdateSourceRect(limb, newRect); + UpdateSourceRect(limb, newRect, resize); } break; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorImage.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorImage.cs new file mode 100644 index 000000000..1b9b5d89f --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorImage.cs @@ -0,0 +1,571 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml.Linq; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; + +namespace Barotrauma +{ + class EditorImageManager + { + private struct EditorImageContainer + { + public float Rotation; + public float Scale; + public Vector2 Position; + public string Path; + public float Opacity; + public EditorImage.DrawTargetType DrawTarget; + + public EditorImage CreateImage() + { + return new EditorImage(Path, Position) + { + Position = Position, + Scale = Scale, + Opacity = Opacity, + Rotation = Rotation, + DrawTarget = DrawTarget + }; + } + + public static EditorImageContainer? Load(XElement element) + { + string path = element.GetAttributeString("path", ""); + if (string.IsNullOrWhiteSpace(path)) { return null; } + + Vector2 pos = element.GetAttributeVector2("pos", Vector2.Zero); + float scale = element.GetAttributeFloat("scale", 1f); + float rotation = element.GetAttributeFloat("rotation", 0f); + float opacity = element.GetAttributeFloat("opacity", 1f); + string drawTargetString = element.GetAttributeString("drawtarget", ""); + if (!Enum.TryParse(drawTargetString, out var drawTarget)) + { + drawTarget = EditorImage.DrawTargetType.World; + } + + return new EditorImageContainer + { + Path = path, + Rotation = rotation, + Opacity = opacity, + Position = pos, + Scale = scale, + DrawTarget = drawTarget + }; + } + + public static EditorImageContainer ImageToContainer(EditorImage img) + { + return new EditorImageContainer + { + Path = img.ImagePath, + Rotation = img.Rotation, + Position = img.Position, + Opacity = img.Opacity, + Scale = img.Scale, + DrawTarget = img.DrawTarget + }; + } + + public static XElement SerializeImage(EditorImageContainer image) + { + return new XElement("image", + new XAttribute("pos", XMLExtensions.Vector2ToString(image.Position)), + new XAttribute("rotation", image.Rotation), + new XAttribute("opacity", image.Opacity), + new XAttribute("path", image.Path), + new XAttribute("scale", image.Scale), + new XAttribute("drawtarget", image.DrawTarget.ToString())); + } + } + + private readonly List PendingImages = new List(); + + public readonly List Images = new List(); + + private readonly List screenImages = new List(), + worldImages = new List(); + + public bool EditorMode; + + private string editModeText = ""; + private Vector2 textSize = Vector2.Zero; + + public void Save(XElement element) + { + XElement saveElement = new XElement("editorimages"); + foreach (EditorImage image in Images) + { + EditorImageContainer container = EditorImageContainer.ImageToContainer(image); + saveElement.Add(EditorImageContainer.SerializeImage(container)); + } + + foreach (EditorImageContainer container in PendingImages) + { + saveElement.Add(EditorImageContainer.SerializeImage(container)); + } + + element.Add(saveElement); + } + + public void Load(XElement element) + { + Clear(alsoPending: true); + + foreach (XElement subElement in element.Elements()) + { + EditorImageContainer? tempImage = EditorImageContainer.Load(subElement); + if (tempImage != null) + { + PendingImages.Add(tempImage.Value); + } + } + } + + public void OnEditorSelected() + { + editModeText = TextManager.Get("SubEditor.ImageEditingMode"); + textSize = GUI.LargeFont.MeasureString(editModeText); + + TryLoadPendingImages(); + } + + private void TryLoadPendingImages() + { + if (PendingImages.Count == 0) { return; } + + Clear(alsoPending: false); + + foreach (EditorImageContainer pendingImage in PendingImages) + { + EditorImage img = pendingImage.CreateImage(); + if (img.Image == null) { continue; } + Images.Add(img); + img.UpdateRectangle(); + } + + UpdateImageCategories(); + PendingImages.Clear(); + } + + public void Clear(bool alsoPending = false) + { + foreach (EditorImage img in Images) + { + img.Image?.Dispose(); + } + + Images.Clear(); + screenImages.Clear(); + worldImages.Clear(); + if (alsoPending) + { + PendingImages.Clear(); + } + } + + public void Update(float deltaTime) + { + if (!EditorMode) { return; } + + foreach (EditorImage image in Images) + { + image.Update(deltaTime); + } + + if (PlayerInput.PrimaryMouseButtonDown()) + { + EditorImage? hover = Images.FirstOrDefault(img => img.IsMouseOn()); + if (hover != null) + { + foreach (EditorImage image in Images) + { + image.Selected = false; + } + + hover.Selected = true; + } + } + + if (PlayerInput.KeyHit(Keys.Delete) || (PlayerInput.IsCtrlDown() && PlayerInput.KeyHit(Keys.D))) + { + Images.RemoveAll(img => img.Selected); + UpdateImageCategories(); + } + + if (PlayerInput.KeyHit(Keys.Space)) + { + foreach (EditorImage image in Images) + { + if (image.Selected) + { + if (image.DrawTarget == EditorImage.DrawTargetType.World) + { + Vector2 pos = image.Position; + pos.Y = -pos.Y; + pos = Screen.Selected.Cam.WorldToScreen(pos); + if (PlayerInput.IsShiftDown()) + { + pos = new Vector2(GameMain.GraphicsWidth / 2f, GameMain.GraphicsHeight / 2f); + } + + image.Position = pos; + image.DrawTarget = EditorImage.DrawTargetType.Camera; + image.Scale *= Screen.Selected.Cam.Zoom; + image.UpdateRectangle(); + } + else + { + Vector2 pos = Screen.Selected.Cam.ScreenToWorld(image.Position); + pos.Y = -pos.Y; + image.Position = pos; + image.DrawTarget = EditorImage.DrawTargetType.World; + image.Scale /= Screen.Selected.Cam.Zoom; + image.UpdateRectangle(); + } + } + } + + UpdateImageCategories(); + } + + MapEntity.DisableSelect = true; + } + + private void UpdateImageCategories() + { + screenImages.Clear(); + worldImages.Clear(); + + foreach (EditorImage image in Images) + { + switch (image.DrawTarget) + { + case EditorImage.DrawTargetType.World: + worldImages.Add(image); + break; + default: + screenImages.Add(image); + break; + } + } + } + + public void CreateImageWizard(Vector2 positon) + { + string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + if (!Directory.Exists(home)) { return; } + + FileSelection.OnFileSelected = file => + { + Vector2 pos = Screen.Selected.Cam.ScreenToWorld(positon); + pos.Y = -pos.Y; + Images.Add(new EditorImage(file, pos) { DrawTarget = EditorImage.DrawTargetType.World }); + UpdateImageCategories(); + GameMain.Config.SaveNewPlayerConfig(); + }; + + FileSelection.ClearFileTypeFilters(); + FileSelection.AddFileTypeFilter("PNG", "*.png"); + FileSelection.AddFileTypeFilter("JPEG", "*.jpg, *.jpeg"); + FileSelection.AddFileTypeFilter("All files", "*.*"); + FileSelection.SelectFileTypeFilter("*.png"); + FileSelection.CurrentDirectory = home; + FileSelection.Open = true; + } + + public void DrawEditing(SpriteBatch spriteBatch, Camera cam) + { + if (!EditorMode) { return; } + + DrawImages(spriteBatch, cam); + + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState); + Vector2 textPos = new Vector2(GameMain.GraphicsWidth / 2f - (textSize.X / 2f), GameMain.GraphicsHeight / 10f - (textSize.Y / 2f)); + GUI.DrawString(spriteBatch, textPos, editModeText, GUI.Style.Yellow, Color.Black * 0.4f, 8, GUI.LargeFont); + spriteBatch.End(); + } + + public void Draw(SpriteBatch spriteBatch, Camera cam) + { + if (EditorMode) { return; } + + DrawImages(spriteBatch, cam); + } + + private void DrawImages(SpriteBatch spriteBatch, Camera cam) + { + if (screenImages.Count > 0) + { + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState); + foreach (EditorImage image in screenImages) + { + image.Draw(spriteBatch); + if (EditorMode) { image.DrawEditing(spriteBatch, cam); } + } + + spriteBatch.End(); + } + + if (worldImages.Count > 0) + { + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, transformMatrix: cam.Transform); + foreach (EditorImage image in worldImages) + { + image.Draw(spriteBatch); + if (EditorMode) { image.DrawEditing(spriteBatch, cam); } + } + + spriteBatch.End(); + } + } + } + + class EditorImage + { + public enum DrawTargetType + { + Camera, + World + } + + public Texture2D? Image; + public string ImagePath; + public Vector2 Position; + public float Rotation; + public float Opacity = 1f; + public float Scale = 1f; + public DrawTargetType DrawTarget; + public bool Selected; + + public Rectangle Bounds; + private float prevAngle; + private bool disableMove; + private bool isDragging; + + private readonly Dictionary widgets = new Dictionary(); + + public EditorImage(string path, Vector2 pos) + { + Image = Sprite.LoadTexture(path, out Sprite _, compress: false); + ImagePath = path; + Position = pos; + UpdateRectangle(); + } + + public bool IsMouseOn() => Bounds.Contains(GetMousePos()); + + public Vector2 GetMousePos() + { + switch (DrawTarget) + { + case DrawTargetType.Camera: + return PlayerInput.MousePosition; + case DrawTargetType.World: + Vector2 pos = Screen.Selected.Cam.ScreenToWorld(PlayerInput.MousePosition); + pos.Y = -pos.Y; + return pos; + default: + return PlayerInput.MousePosition; + } + } + + public void Update(float deltaTime) + { + if (!Selected) { return; } + + if (widgets.Values.Any(w => w.IsSelected)) { return; } + + if (PlayerInput.PrimaryMouseButtonDown() && !disableMove && IsMouseOn()) + { + isDragging = true; + } + + if (isDragging) + { + Camera cam = Screen.Selected.Cam; + if (PlayerInput.MouseSpeed != Vector2.Zero) + { + Vector2 mouseSpeed = PlayerInput.MouseSpeed; + if (DrawTarget == DrawTargetType.World) + { + mouseSpeed /= cam.Zoom; + } + + Position += mouseSpeed; + UpdateRectangle(); + } + } + + if (PlayerInput.KeyDown(Keys.OemPlus) || PlayerInput.KeyDown(Keys.Up)) + { + Opacity += 0.01f; + } + + if (PlayerInput.KeyDown(Keys.OemMinus) || PlayerInput.KeyDown(Keys.Down)) + { + Opacity -= 0.01f; + } + + if (PlayerInput.KeyHit(Keys.D0)) + { + Opacity = 1f; + } + + Opacity = Math.Clamp(Opacity, 0, 1f); + + if (!PlayerInput.PrimaryMouseButtonHeld()) + { + isDragging = false; + } + } + + private void DrawWidgets(SpriteBatch spriteBatch) + { + float widgetSize = Image == null ? 100f : Math.Max(Image.Width, Image.Height) / 2f; + + int width = 3; + int size = 32; + if (DrawTarget == DrawTargetType.World) + { + width = Math.Max(width, (int) (width / Screen.Selected.Cam.Zoom)); + } + + Widget currentWidget = GetWidget("transform", size, width, widget => + { + widget.MouseDown += () => + { + widget.color = GUI.Style.Green; + prevAngle = Rotation; + disableMove = true; + }; + widget.Deselected += () => + { + widget.color = Color.Yellow; + disableMove = false; + }; + widget.MouseHeld += (deltaTime) => + { + Rotation = GetRotationAngle(Position) + (float) Math.PI / 2f; + float distance = Vector2.Distance(Position, GetMousePos()); + Scale = Math.Abs(distance) / widgetSize; + if (PlayerInput.IsShiftDown()) + { + const float rotationStep = (float) (Math.PI / 4f); + Rotation = (float) Math.Round(Rotation / rotationStep) * rotationStep; + } + + if (PlayerInput.IsCtrlDown()) + { + const float scaleStep = 0.1f; + Scale = (float) Math.Round(Scale / scaleStep) * scaleStep; + } + + UpdateRectangle(); + }; + widget.PreUpdate += (deltaTime) => + { + if (DrawTarget != DrawTargetType.World) { return; } + + widget.DrawPos = new Vector2(widget.DrawPos.X, -widget.DrawPos.Y); + widget.DrawPos = Screen.Selected.Cam.WorldToScreen(widget.DrawPos); + }; + widget.PostUpdate += (deltaTime) => + { + if (DrawTarget != DrawTargetType.World) { return; } + + widget.DrawPos = Screen.Selected.Cam.ScreenToWorld(widget.DrawPos); + widget.DrawPos = new Vector2(widget.DrawPos.X, -widget.DrawPos.Y); + }; + widget.PreDraw += (sprtBtch, deltaTime) => + { + widget.tooltip = $"Scale: {Math.Round(Scale, 2)}\n" + + $"Rotation: {(int) MathHelper.ToDegrees(Rotation)}"; + float rotation = Rotation - (float) Math.PI / 2f; + widget.DrawPos = Position + new Vector2((float) Math.Cos(rotation), (float) Math.Sin(rotation)) * (Scale * widgetSize); + widget.Update(deltaTime); + }; + }); + + currentWidget.Draw(spriteBatch, (float) Timing.Step); + GUI.DrawLine(spriteBatch, Position, currentWidget.DrawPos, GUI.Style.Green, width: width); + } + + private float GetRotationAngle(Vector2 drawPosition) + { + Vector2 rotationVector = GetMousePos() - drawPosition; + rotationVector.Normalize(); + double angle = Math.Atan2(MathHelper.ToRadians(rotationVector.Y), MathHelper.ToRadians(rotationVector.X)); + if (angle < 0) + { + angle = Math.Abs(angle - prevAngle) < Math.Abs((angle + Math.PI * 2) - prevAngle) ? angle : angle + Math.PI * 2; + } + else if (angle > 0) + { + angle = Math.Abs(angle - prevAngle) < Math.Abs((angle - Math.PI * 2) - prevAngle) ? angle : angle - Math.PI * 2; + } + + angle = MathHelper.Clamp((float) angle, -((float) Math.PI * 2), (float) Math.PI * 2); + prevAngle = (float) angle; + return (float) angle; + } + + private Widget GetWidget(string id, int size, float thickness = 1f, Action? initMethod = null) + { + if (!widgets.TryGetValue(id, out Widget? widget)) + { + widget = new Widget(id, size, Widget.Shape.Rectangle) + { + color = Color.Yellow, + RequireMouseOn = false + }; + widgets.Add(id, widget); + initMethod?.Invoke(widget); + } + + widget.size = size; + widget.thickness = thickness; + return widget; + } + + public void UpdateRectangle() + { + if (Image == null) + { + Bounds = new Rectangle((int) Position.X, (int) Position.Y, 512, 512); + return; + } + + Vector2 size = new Vector2(Image.Width * Scale, Image.Height * Scale); + Bounds = new Rectangle((Position - size / 2f).ToPoint(), size.ToPoint()); + } + + public void Draw(SpriteBatch spriteBatch) + { + if (Image == null) { return; } + + spriteBatch.Draw(Image, Position, null, Color.White * Opacity, Rotation, new Vector2(Image.Width / 2f, Image.Height / 2f), scale: Scale, SpriteEffects.None, 0f); + } + + public void DrawEditing(SpriteBatch spriteBatch, Camera cam) + { + Rectangle bounds = Bounds; + int width = 4; + if (DrawTarget == DrawTargetType.World) + { + width = (int) (width / cam.Zoom); + } + + GUI.DrawRectangle(spriteBatch, bounds, Selected ? GUI.Style.Red : GUI.Style.Green, thickness: width); + if (Selected) + { + DrawWidgets(spriteBatch); + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs index 27edd2765..80361fede 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs @@ -399,7 +399,7 @@ namespace Barotrauma else { newNode = new CustomNode(subElement.Name.ToString()) { Position = new Vector2(ident, 0), ID = CreateID() }; - foreach (XAttribute attribute in subElement.Attributes()) + foreach (XAttribute attribute in subElement.Attributes().Where(attribute => !attribute.ToString().StartsWith("_"))) { newNode.Connections.Add(new NodeConnection(newNode, NodeConnectionType.Value, attribute.Name.ToString(), typeof(string))); } @@ -525,6 +525,7 @@ namespace Barotrauma public override void Select() { + GUI.PreventPauseMenuToggle = false; projectName = TextManager.Get("EventEditor.Unnamed"); base.Select(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs index f2b645ef3..42076beb3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs @@ -77,9 +77,8 @@ namespace Barotrauma } if (Character.Controlled?.Inventory != null) { - foreach (Item item in Character.Controlled.Inventory.Items) + foreach (Item item in Character.Controlled.Inventory.AllItems) { - if (item == null) { continue; } if (Character.Controlled.HasEquippedItem(item)) { item.AddToGUIUpdateList(); @@ -249,6 +248,8 @@ namespace Barotrauma } spriteBatch.End(); + Level.Loaded?.DrawFront(spriteBatch, cam); + //draw the rendertarget and particles that are only supposed to be drawn in water into renderTargetWater graphics.SetRenderTarget(renderTargetWater); @@ -317,10 +318,8 @@ namespace Barotrauma { c.DrawFront(spriteBatch, cam); } - if (Level.Loaded != null) - { - Level.Loaded.DrawFront(spriteBatch, cam); - } + + Level.Loaded?.DrawDebugOverlay(spriteBatch, cam); if (GameMain.DebugDraw) { MapEntity.mapEntityList.ForEach(me => me.AiTarget?.Draw(spriteBatch)); @@ -374,7 +373,10 @@ namespace Barotrauma { BlurStrength = Character.Controlled.BlurStrength * 0.005f; DistortStrength = Character.Controlled.DistortStrength; - chromaticAberrationStrength -= Vector3.One * Character.Controlled.RadialDistortStrength; + if (GameMain.Config.EnableRadialDistortion) + { + chromaticAberrationStrength -= Vector3.One * Character.Controlled.RadialDistortStrength; + } chromaticAberrationStrength += new Vector3(-0.03f, -0.015f, 0.0f) * Character.Controlled.ChromaticAberrationStrength; } else @@ -438,8 +440,8 @@ namespace Barotrauma if (!PlayerInput.PrimaryMouseButtonHeld()) { - Inventory.draggingSlot = null; - Inventory.draggingItem = null; + Inventory.DraggingSlot = null; + Inventory.DraggingItems.Clear(); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs index c36d6d6d5..657f66ea8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs @@ -714,8 +714,9 @@ namespace Barotrauma if (Level.Loaded != null) { Level.Loaded.DrawBack(graphics, spriteBatch, cam); - spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, SamplerState.LinearWrap, DepthStencilState.DepthRead, transformMatrix: cam.Transform); Level.Loaded.DrawFront(spriteBatch, cam); + spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, SamplerState.LinearWrap, DepthStencilState.DepthRead, transformMatrix: cam.Transform); + Level.Loaded.DrawDebugOverlay(spriteBatch, cam); Submarine.Draw(spriteBatch, false); Submarine.DrawFront(spriteBatch); Submarine.DrawDamageable(spriteBatch, null); @@ -817,6 +818,13 @@ namespace Barotrauma public override void Update(double deltaTime) { + if (lightingEnabled.Selected) + { + foreach (Item item in Item.ItemList) + { + item?.GetComponent()?.Update((float)deltaTime, cam); + } + } GameMain.LightManager?.Update((float)deltaTime); pointerLightSource.Position = cam.ScreenToWorld(PlayerInput.MousePosition); @@ -886,16 +894,16 @@ namespace Barotrauma { foreach (XElement subElement in element.Elements()) { - string id = element.GetAttributeString("identifier", null) ?? element.Name.ToString(); + string id = subElement.GetAttributeString("identifier", null) ?? subElement.Name.ToString(); if (!id.Equals(genParams.Name, StringComparison.OrdinalIgnoreCase)) { continue; } - SerializableProperty.SerializeProperties(genParams, element, true); + genParams.Save(subElement); } } else { string id = element.GetAttributeString("identifier", null) ?? element.Name.ToString(); if (!id.Equals(genParams.Name, StringComparison.OrdinalIgnoreCase)) { continue; } - SerializableProperty.SerializeProperties(genParams, element, true); + genParams.Save(element); } break; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index b4e064267..8fa32e1b4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -679,7 +679,7 @@ namespace Barotrauma } #endregion - public void QuickStart(bool fixedSeed = false) + public void QuickStart(bool fixedSeed = false, string sub = null) { if (fixedSeed) { @@ -688,7 +688,7 @@ namespace Barotrauma } SubmarineInfo selectedSub = null; - string subName = GameMain.Config.QuickStartSubmarineName; + string subName = sub ?? GameMain.Config.QuickStartSubmarineName; if (!string.IsNullOrEmpty(subName)) { DebugConsole.NewMessage($"Loading the predefined quick start sub \"{subName}\"", Color.White); @@ -834,6 +834,39 @@ namespace Barotrauma return true; } + private void TryStartServer() + { + if (SubmarineInfo.SavedSubmarines.Any(s => s.CalculatingHash)) + { + var waitBox = new GUIMessageBox(TextManager.Get("pleasewait"), TextManager.Get("waitforsubmarinehashcalculations"), new string[] { TextManager.Get("cancel") }); + var waitCoroutine = CoroutineManager.StartCoroutine(WaitForSubmarineHashCalculations(waitBox), "WaitForSubmarineHashCalculations"); + waitBox.Buttons[0].OnClicked += (btn, userdata) => + { + CoroutineManager.StopCoroutines(waitCoroutine); + return true; + }; + } + else + { + StartServer(); + } + } + + private IEnumerable WaitForSubmarineHashCalculations(GUIMessageBox messageBox) + { + string originalText = messageBox.Text.Text; + int doneCount = 0; + do + { + doneCount = SubmarineInfo.SavedSubmarines.Count(s => !s.CalculatingHash); + messageBox.Text.Text = originalText + $" ({doneCount}/{SubmarineInfo.SavedSubmarines.Count()})"; + yield return CoroutineStatus.Running; + } while (doneCount < SubmarineInfo.SavedSubmarines.Count()); + messageBox.Close(); + StartServer(); + yield return CoroutineStatus.Success; + } + private void StartServer() { string name = serverNameBox.Text; @@ -1095,12 +1128,13 @@ namespace Barotrauma StartNewGame = StartGame }; - var startButtonContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), innerNewGame.RectTransform, Anchor.Center), style: null); + var startButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), innerNewGame.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.BottomRight); campaignSetupUI.StartButton.RectTransform.Parent = startButtonContainer.RectTransform; campaignSetupUI.StartButton.RectTransform.MinSize = new Point( (int)(campaignSetupUI.StartButton.TextBlock.TextSize.X * 1.5f), campaignSetupUI.StartButton.RectTransform.MinSize.Y); startButtonContainer.RectTransform.MinSize = new Point(0, campaignSetupUI.StartButton.RectTransform.MinSize.Y); + campaignSetupUI.InitialMoneyText.RectTransform.Parent = startButtonContainer.RectTransform; } private void CreateHostServerFields() @@ -1340,7 +1374,7 @@ namespace Barotrauma new string[] { TextManager.Get("yes"), TextManager.Get("no") }); msgBox.Buttons[0].OnClicked += (_, __) => { - StartServer(); + TryStartServer(); msgBox.Close(); return true; }; @@ -1348,7 +1382,7 @@ namespace Barotrauma } else { - StartServer(); + TryStartServer(); } return true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index d155c8c03..cc0a43c17 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -193,6 +193,12 @@ namespace Barotrauma private set; } + public GUIListBox TeamPreferenceListBox + { + get; + private set; + } + public GUIButton StartButton { get; @@ -1230,24 +1236,23 @@ namespace Barotrauma { tickBox.Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); } - traitorProbabilityButtons[0].Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); - traitorProbabilityButtons[1].Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); - botCountButtons[0].Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); - botCountButtons[1].Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); - botSpawnModeButtons[0].Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); - botSpawnModeButtons[1].Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); - levelDifficultyScrollBar.Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); + SeedBox.Enabled = !CampaignFrame.Visible && !CampaignSetupFrame.Visible && GameMain.Client.HasPermission(ClientPermissions.ManageSettings); + levelDifficultyScrollBar.Enabled = !CampaignFrame.Visible && !CampaignSetupFrame.Visible && GameMain.Client.HasPermission(ClientPermissions.ManageSettings); + traitorProbabilityButtons[0].Enabled = traitorProbabilityButtons[1].Enabled = traitorProbabilityText.Enabled = + !CampaignFrame.Visible && !CampaignSetupFrame.Visible && GameMain.Client.HasPermission(ClientPermissions.ManageSettings); + botCountButtons[0].Enabled = botCountButtons[1].Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); + botSpawnModeButtons[0].Enabled = botSpawnModeButtons[1].Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); + autoRestartBox.Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); - SeedBox.Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); SettingsButton.Visible = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); SettingsButton.OnClicked = GameMain.Client.ServerSettings.ToggleSettingsFrame; StartButton.Visible = GameMain.Client.HasPermission(ClientPermissions.ManageRound) && !GameMain.Client.GameStarted && !CampaignSetupFrame.Visible && !CampaignFrame.Visible; ServerName.Readonly = !GameMain.Client.HasPermission(ClientPermissions.ManageSettings); ServerMessage.Readonly = !GameMain.Client.HasPermission(ClientPermissions.ManageSettings); - shuttleTickBox.Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); - SubList.Enabled = !CampaignFrame.Visible && (GameMain.Client.ServerSettings.Voting.AllowSubVoting || GameMain.Client.HasPermission(ClientPermissions.SelectSub)); - shuttleList.Enabled = shuttleTickBox.Enabled = !CampaignFrame.Visible && GameMain.Client.HasPermission(ClientPermissions.SelectSub); + shuttleTickBox.Enabled = !CampaignFrame.Visible && !CampaignSetupFrame.Visible && GameMain.Client.HasPermission(ClientPermissions.ManageSettings); + SubList.Enabled = !CampaignFrame.Visible && (GameMain.Client.ServerSettings.Voting.AllowSubVoting || GameMain.Client.HasPermission(ClientPermissions.SelectSub)); + shuttleList.Enabled = shuttleList.ButtonEnabled = shuttleTickBox.Enabled = !CampaignFrame.Visible && !CampaignSetupFrame.Visible && GameMain.Client.HasPermission(ClientPermissions.SelectSub); ModeList.Enabled = GameMain.Client.ServerSettings.Voting.AllowModeVoting || GameMain.Client.HasPermission(ClientPermissions.SelectMode); LogButtons.Visible = GameMain.Client.HasPermission(ClientPermissions.ServerLog); GameMain.Client.ShowLogButton.Visible = GameMain.Client.HasPermission(ClientPermissions.ServerLog); @@ -1373,9 +1378,9 @@ namespace Barotrauma }; }; - new GUICustomComponent(new RectTransform(new Vector2(0.6f, 0.18f), infoContainer.RectTransform, Anchor.TopCenter), + new GUICustomComponent(new RectTransform(new Vector2(0.6f, 0.16f), infoContainer.RectTransform, Anchor.TopCenter), onDraw: (sb, component) => characterInfo.DrawIcon(sb, component.Rect.Center.ToVector2(), targetAreaSize: component.Rect.Size.ToVector2())); - + if (allowEditing) { GUILayoutGroup characterInfoTabs = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.04f), infoContainer.RectTransform), isHorizontal: true) @@ -1488,6 +1493,78 @@ namespace Barotrauma } }; } + + TeamPreferenceListBox = null; + if (SelectedMode == GameModePreset.PvP) + { + TeamPreferenceListBox = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.04f), infoContainer.RectTransform, anchor: Anchor.TopLeft, pivot: Pivot.TopLeft), isHorizontal: true, style: null) + { + Enabled = true, + KeepSpaceForScrollBar = false, + ScrollBarEnabled = false, + ScrollBarVisible = false + }; + + TeamPreferenceListBox.UpdateDimensions(); + + Color team1Color = new Color(0, 110, 150, 255); + var team1Option = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1.0f), TeamPreferenceListBox.Content.RectTransform), TextManager.Get("teampreference.team1"), textAlignment: Alignment.Center, style: null) + { + UserData = CharacterTeamType.Team1, + CanBeFocused = true, + Padding = Vector4.One * 10.0f * GUI.Scale, + Color = Color.Lerp(team1Color, Color.Black, 0.7f) * 0.7f, + HoverColor = team1Color * 0.95f, + SelectedColor = team1Color * 0.8f, + OutlineColor = team1Color, + TextColor = Color.White, + HoverTextColor = Color.White, + SelectedTextColor = Color.White + }; + + Color noPreferenceColor = new Color(100, 100, 100, 255); + var noPreferenceOption = new GUITextBlock(new RectTransform(new Vector2(0.4f, 1.0f), TeamPreferenceListBox.Content.RectTransform), TextManager.Get("teampreference.nopreference"), textAlignment: Alignment.Center, style: null) + { + UserData = CharacterTeamType.None, + CanBeFocused = true, + Padding = Vector4.One * 10.0f * GUI.Scale, + Color = Color.Lerp(noPreferenceColor, Color.Black, 0.7f) * 0.7f, + HoverColor = noPreferenceColor * 0.95f, + SelectedColor = noPreferenceColor * 0.8f, + OutlineColor = noPreferenceColor, + TextColor = Color.White, + HoverTextColor = Color.White, + SelectedTextColor = Color.White + }; + + Color team2Color = new Color(150, 110, 0, 255); + var team2Option = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1.0f), TeamPreferenceListBox.Content.RectTransform), TextManager.Get("teampreference.team2"), textAlignment: Alignment.Center, style: null) + { + UserData = CharacterTeamType.Team2, + CanBeFocused = true, + Padding = Vector4.One * 10.0f * GUI.Scale, + Color = Color.Lerp(team2Color, Color.Black, 0.7f) * 0.7f, + HoverColor = team2Color * 0.95f, + SelectedColor = team2Color * 0.8f, + OutlineColor = team2Color, + TextColor = Color.White, + HoverTextColor = Color.White, + SelectedTextColor = Color.White + }; + + TeamPreferenceListBox.Select(GameMain.Config.TeamPreference); + + TeamPreferenceListBox.OnSelected += (component, obj) => + { + if ((CharacterTeamType)obj == GameMain.Config.TeamPreference) { return true; } + + GameMain.Config.TeamPreference = (CharacterTeamType)obj; + GameMain.Client.ForceNameAndJobUpdate(); + GameMain.Config.SaveNewPlayerConfig(); + + return true; + }; + } } private void CreateChangesPendingText() @@ -1749,6 +1826,15 @@ namespace Barotrauma } GameMain.Client.RequestSelectMode(component.Parent.GetChildIndex(component)); HighlightMode(SelectedModeIndex); + + if (presetName.Equals("multiplayercampaign", StringComparison.OrdinalIgnoreCase)) + { + GUI.SetCursorWaiting(endCondition: () => + { + return CampaignFrame.Visible || CampaignSetupFrame.Visible; + }); + } + return !presetName.Equals("multiplayercampaign", StringComparison.OrdinalIgnoreCase); } return false; @@ -1819,7 +1905,19 @@ namespace Barotrauma playerFrame.Text = client.Name; Color color = Color.White; - if (JobPrefab.Prefabs.ContainsKey(client.PreferredJob)) + if (SelectedMode == GameModePreset.PvP) + { + switch (client.PreferredTeam) + { + case CharacterTeamType.Team1: + color = new Color(0, 110, 150, 255); + break; + case CharacterTeamType.Team2: + color = new Color(150, 110, 0, 255); + break; + } + } + else if (JobPrefab.Prefabs.ContainsKey(client.PreferredJob)) { color = JobPrefab.Prefabs[client.PreferredJob].UIColor; } @@ -2104,42 +2202,44 @@ namespace Barotrauma rangebanButton.OnClicked += ClosePlayerFrame; } - - if (GameMain.Client != null && GameMain.Client.ServerSettings.Voting.AllowVoteKick && - selectedClient != null && selectedClient.AllowKicking) + if (GameMain.Client != null && GameMain.Client.ConnectedClients.Contains(selectedClient)) { - var kickVoteButton = new GUIButton(new RectTransform(new Vector2(0.34f, 1.0f), buttonAreaLower.RectTransform), - TextManager.Get("VoteToKick")) + if (GameMain.Client.ServerSettings.Voting.AllowVoteKick && + selectedClient != null && selectedClient.AllowKicking) { - Enabled = !selectedClient.HasKickVoteFromID(GameMain.Client.ID), - OnClicked = (btn, userdata) => { GameMain.Client.VoteForKick(selectedClient); btn.Enabled = false; return true; }, - UserData = selectedClient - }; - } + var kickVoteButton = new GUIButton(new RectTransform(new Vector2(0.34f, 1.0f), buttonAreaLower.RectTransform), + TextManager.Get("VoteToKick")) + { + Enabled = !selectedClient.HasKickVoteFromID(GameMain.Client.ID), + OnClicked = (btn, userdata) => { GameMain.Client.VoteForKick(selectedClient); btn.Enabled = false; return true; }, + UserData = selectedClient + }; + } - if (GameMain.Client.HasPermission(ClientPermissions.Kick) && - selectedClient != null && selectedClient.AllowKicking) - { - var kickButton = new GUIButton(new RectTransform(new Vector2(0.34f, 1.0f), buttonAreaLower.RectTransform), - TextManager.Get("Kick")) + if (GameMain.Client.HasPermission(ClientPermissions.Kick) && + selectedClient != null && selectedClient.AllowKicking) { - UserData = selectedClient + var kickButton = new GUIButton(new RectTransform(new Vector2(0.34f, 1.0f), buttonAreaLower.RectTransform), + TextManager.Get("Kick")) + { + UserData = selectedClient + }; + kickButton.OnClicked = (bt, userdata) => { KickPlayer(selectedClient); return true; }; + kickButton.OnClicked += ClosePlayerFrame; + } + + new GUITickBox(new RectTransform(new Vector2(0.175f, 1.0f), headerContainer.RectTransform, Anchor.TopRight), + TextManager.Get("Mute")) + { + Selected = selectedClient.MutedLocally, + OnSelected = (tickBox) => { selectedClient.MutedLocally = tickBox.Selected; return true; } }; - kickButton.OnClicked = (bt, userdata) => { KickPlayer(selectedClient); return true; }; - kickButton.OnClicked += ClosePlayerFrame; } if (buttonAreaTop.CountChildren > 0) { GUITextBlock.AutoScaleAndNormalize(buttonAreaTop.Children.Select(c => ((GUIButton)c).TextBlock).Concat(buttonAreaLower.Children.Select(c => ((GUIButton)c).TextBlock))); } - - new GUITickBox(new RectTransform(new Vector2(0.175f, 1.0f), headerContainer.RectTransform, Anchor.TopRight), - TextManager.Get("Mute")) - { - Selected = selectedClient.MutedLocally, - OnSelected = (tickBox) => { selectedClient.MutedLocally = tickBox.Selected; return true; } - }; } if (selectedClient.SteamID != 0 && Steam.SteamManager.IsInitialized) @@ -2983,16 +3083,25 @@ namespace Barotrauma { ToggleCampaignMode(false); } - + + var prevMode = modeList.Content.GetChild(selectedModeIndex).UserData as GameModePreset; + if ((HighlightedModeIndex == selectedModeIndex || HighlightedModeIndex < 0) && modeList.SelectedIndex != modeIndex) { modeList.Select(modeIndex, true); } selectedModeIndex = modeIndex; + if ((prevMode == GameModePreset.PvP) != (SelectedMode == GameModePreset.PvP)) + { + UpdatePlayerFrame(null); + GameMain.Client.ConnectedClients.ForEach(c => SetPlayerNameAndJobPreference(c)); + } + if (SelectedMode != GameModePreset.MultiPlayerCampaign && GameMain.GameSession?.GameMode is CampaignMode && Selected == this) { GameMain.GameSession = null; } RefreshGameModeContent(); + RefreshEnabledElements(); } public void HighlightMode(int modeIndex) @@ -3001,6 +3110,7 @@ namespace Barotrauma HighlightedModeIndex = modeIndex; RefreshGameModeContent(); + RefreshEnabledElements(); } private void RefreshMissionTypes() @@ -3047,7 +3157,13 @@ namespace Barotrauma else { CampaignFrame.Visible = false; - CampaignSetupFrame.Visible = GameMain.Client.HasPermission(ClientPermissions.ManageCampaign); + CampaignSetupFrame.Visible = true; + if (!GameMain.Client.HasPermission(ClientPermissions.ManageCampaign)) + { + CampaignSetupFrame.ClearChildren(); + new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.5f), CampaignSetupFrame.RectTransform, Anchor.Center), + TextManager.Get("campaignstarting"), font: GUI.SubHeadingFont, textAlignment: Alignment.Center, wrap: true); + } } } else diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs index 5475c5bc2..f408c64ba 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs @@ -151,8 +151,9 @@ namespace Barotrauma private GUITickBox filterTraitor; private GUITickBox filterModded; private GUITickBox filterVoip; - private List playStyleTickBoxes; - private List gameModeTickBoxes; + private Dictionary filterTickBoxes; + private Dictionary playStyleTickBoxes; + private Dictionary gameModeTickBoxes; private GUITickBox filterOffensive; private string sortedBy; @@ -322,7 +323,7 @@ namespace Barotrauma }; filterToggle.Children.ForEach(c => c.SpriteEffects = SpriteEffects.FlipHorizontally); - List filterTextList = new List(); + filterTickBoxes = new Dictionary(); filterSameVersion = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("FilterSameVersion")) { @@ -330,42 +331,42 @@ namespace Barotrauma Selected = true, OnSelected = (tickBox) => { FilterServers(); return true; } }; - filterTextList.Add(filterSameVersion.TextBlock); + filterTickBoxes.Add("FilterSameVersion", filterSameVersion); filterPassword = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("FilterPassword")) { UserData = TextManager.Get("FilterPassword"), OnSelected = (tickBox) => { FilterServers(); return true; } }; - filterTextList.Add(filterPassword.TextBlock); + filterTickBoxes.Add("FilterPassword", filterPassword); filterIncompatible = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("FilterIncompatibleServers")) { UserData = TextManager.Get("FilterIncompatibleServers"), OnSelected = (tickBox) => { FilterServers(); return true; } }; - filterTextList.Add(filterIncompatible.TextBlock); + filterTickBoxes.Add("FilterIncompatibleServers", filterIncompatible); filterFull = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("FilterFullServers")) { UserData = TextManager.Get("FilterFullServers"), OnSelected = (tickBox) => { FilterServers(); return true; } }; - filterTextList.Add(filterFull.TextBlock); + filterTickBoxes.Add("FilterFullServers", filterFull); filterEmpty = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("FilterEmptyServers")) { UserData = TextManager.Get("FilterEmptyServers"), OnSelected = (tickBox) => { FilterServers(); return true; } }; - filterTextList.Add(filterEmpty.TextBlock); + filterTickBoxes.Add("FilterEmptyServers", filterEmpty); filterWhitelisted = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("FilterWhitelistedServers")) { UserData = TextManager.Get("FilterWhitelistedServers"), OnSelected = (tickBox) => { FilterServers(); return true; } }; - filterTextList.Add(filterWhitelisted.TextBlock); + filterTickBoxes.Add("FilterWhitelistedServers", filterWhitelisted); filterOffensive = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("FilterOffensiveServers")) { @@ -373,7 +374,7 @@ namespace Barotrauma ToolTip = TextManager.Get("FilterOffensiveServersToolTip"), OnSelected = (tickBox) => { FilterServers(); return true; } }; - filterTextList.Add(filterOffensive.TextBlock); + filterTickBoxes.Add("FilterOffensiveServers", filterOffensive); // Filter Tags new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("servertags"), font: GUI.SubHeadingFont) @@ -386,35 +387,35 @@ namespace Barotrauma UserData = TextManager.Get("servertag.karma.true"), OnSelected = (tickBox) => { FilterServers(); return true; } }; - filterTextList.Add(filterKarma.TextBlock); + filterTickBoxes.Add("servertag.karma", filterKarma); filterTraitor = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("servertag.traitors.true")) { UserData = TextManager.Get("servertag.traitors.true"), OnSelected = (tickBox) => { FilterServers(); return true; } }; - filterTextList.Add(filterTraitor.TextBlock); + filterTickBoxes.Add("servertag.traitors", filterTraitor); filterFriendlyFire = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("servertag.friendlyfire.false")) { UserData = TextManager.Get("servertag.friendlyfire.false"), OnSelected = (tickBox) => { FilterServers(); return true; } }; - filterTextList.Add(filterFriendlyFire.TextBlock); + filterTickBoxes.Add("servertag.friendlyfire", filterFriendlyFire); filterVoip = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("servertag.voip.false")) { UserData = TextManager.Get("servertag.voip.false"), OnSelected = (tickBox) => { FilterServers(); return true; } }; - filterTextList.Add(filterVoip.TextBlock); - + filterTickBoxes.Add("servertag.voip", filterVoip); + filterModded = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("servertag.modded.true")) { UserData = TextManager.Get("servertag.modded.true"), OnSelected = (tickBox) => { FilterServers(); return true; } }; - filterTextList.Add(filterModded.TextBlock); + filterTickBoxes.Add("servertag.modded", filterModded); // Play Style Selection new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("ServerSettingsPlayStyle"), font: GUI.SubHeadingFont) @@ -422,7 +423,7 @@ namespace Barotrauma CanBeFocused = false }; - playStyleTickBoxes = new List(); + playStyleTickBoxes = new Dictionary(); foreach (PlayStyle playStyle in Enum.GetValues(typeof(PlayStyle))) { var selectionTick = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("servertag." + playStyle)) @@ -432,14 +433,14 @@ namespace Barotrauma OnSelected = (tickBox) => { FilterServers(); return true; }, UserData = playStyle }; - playStyleTickBoxes.Add(selectionTick); - filterTextList.Add(selectionTick.TextBlock); + playStyleTickBoxes.Add("servertag." + playStyle, selectionTick); + filterTickBoxes.Add("servertag." + playStyle, selectionTick); } // Game mode Selection new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("gamemode"), font: GUI.SubHeadingFont) { CanBeFocused = false }; - gameModeTickBoxes = new List(); + gameModeTickBoxes = new Dictionary(); foreach (GameModePreset mode in GameModePreset.List) { if (mode.IsSinglePlayer) continue; @@ -451,21 +452,21 @@ namespace Barotrauma OnSelected = (tickBox) => { FilterServers(); return true; }, UserData = mode.Identifier }; - gameModeTickBoxes.Add(selectionTick); - filterTextList.Add(selectionTick.TextBlock); + gameModeTickBoxes.Add(mode.Identifier, selectionTick); + filterTickBoxes.Add(mode.Identifier, selectionTick); } filters.Content.RectTransform.SizeChanged += () => { filters.Content.RectTransform.RecalculateChildren(true, true); - filterTextList.ForEach(t => t.Text = t.Parent.Parent.UserData as string); - gameModeTickBoxes.ForEach(tb => tb.Text = tb.ToolTip); - playStyleTickBoxes.ForEach(tb => tb.Text = tb.ToolTip); - GUITextBlock.AutoScaleAndNormalize(filterTextList, defaultScale: 1.0f); - if (filterTextList[0].TextScale < 0.8f) + filterTickBoxes.ForEach(t => t.Value.Text = t.Value.UserData as string); + gameModeTickBoxes.ForEach(tb => tb.Value.Text = tb.Value.ToolTip); + playStyleTickBoxes.ForEach(tb => tb.Value.Text = tb.Value.ToolTip); + GUITextBlock.AutoScaleAndNormalize(filterTickBoxes.Values.Select(tb => tb.TextBlock), defaultScale: 1.0f); + if (filterTickBoxes.Values.First().TextBlock.TextScale < 0.8f) { - filterTextList.ForEach(t => t.TextScale = 1.0f); - filterTextList.ForEach(t => t.Text = ToolBox.LimitString(t.Text, t.Font, (int)(filters.Content.Rect.Width * 0.8f))); + filterTickBoxes.ForEach(t => t.Value.TextBlock.TextScale = 1.0f); + filterTickBoxes.ForEach(t => t.Value.TextBlock.Text = ToolBox.LimitString(t.Value.TextBlock.Text, t.Value.TextBlock.Font, (int)(filters.Content.Rect.Width * 0.8f))); } }; @@ -959,6 +960,19 @@ namespace Barotrauma { base.Select(); SelectedTab = ServerListTab.All; + LoadServerFilters(GameMain.Config.ServerFilterElement); + if (GameSettings.ShowOffensiveServerPrompt) + { + var filterOffensivePrompt = new GUIMessageBox(string.Empty, TextManager.Get("filteroffensiveserversprompt"), new string[] { TextManager.Get("yes"), TextManager.Get("no") }); + filterOffensivePrompt.Buttons[0].OnClicked = (btn, userData) => + { + filterOffensive.Selected = true; + filterOffensivePrompt.Close(); + return true; + }; + filterOffensivePrompt.Buttons[1].OnClicked = filterOffensivePrompt.Close; + GameSettings.ShowOffensiveServerPrompt = false; + } Steamworks.SteamMatchmaking.ResetActions(); @@ -975,6 +989,8 @@ namespace Barotrauma { base.Deselect(); + GameMain.Config.SaveNewPlayerConfig(); + pendingWorkshopDownloads?.Clear(); workshopDownloadsFrame = null; } @@ -1083,10 +1099,9 @@ namespace Barotrauma (selectedTab == ServerListTab.Favorites && serverInfo.Favorite)); } - foreach (GUITickBox tickBox in playStyleTickBoxes) + foreach (GUITickBox tickBox in playStyleTickBoxes.Values) { var playStyle = (PlayStyle)tickBox.UserData; - if (!tickBox.Selected && (serverInfo.PlayStyle == playStyle || !serverInfo.PlayStyle.HasValue)) { child.Visible = false; @@ -1094,7 +1109,7 @@ namespace Barotrauma } } - foreach (GUITickBox tickBox in gameModeTickBoxes) + foreach (GUITickBox tickBox in gameModeTickBoxes.Values) { var gameMode = (string)tickBox.UserData; if (!tickBox.Selected && serverInfo.GameMode != null && serverInfo.GameMode.Equals(gameMode, StringComparison.OrdinalIgnoreCase)) @@ -1270,20 +1285,50 @@ namespace Barotrauma if (info.InServer) { + int framePadding = 5; + friendPopup = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas)); - var serverNameText = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), friendPopup.RectTransform), info.ConnectName ?? "[Unnamed]"); - var joinButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), friendPopup.RectTransform, Anchor.TopRight), TextManager.Get("ServerListJoin")) + + var serverNameText = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), friendPopup.RectTransform, Anchor.CenterLeft), info.ConnectName ?? "[Unnamed]"); + serverNameText.RectTransform.AbsoluteOffset = new Point(framePadding, 0); + + var joinButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), friendPopup.RectTransform, Anchor.CenterRight), TextManager.Get("ServerListJoin")) { UserData = info }; joinButton.OnClicked = JoinFriend; + joinButton.RectTransform.AbsoluteOffset = new Point(framePadding, 0); - Vector2 frameDims = joinButton.Font.MeasureString(info.ConnectName ?? "[Unnamed]"); - frameDims.X /= 0.6f; - frameDims.Y *= 1.5f; - friendPopup.RectTransform.NonScaledSize = frameDims.ToPoint(); + Point joinButtonTextSize = joinButton.Font.MeasureString(joinButton.Text).ToPoint(); + int joinButtonHeight = joinButton.RectTransform.NonScaledSize.Y; + int totalAdditionalTextPadding = (joinButtonHeight - joinButtonTextSize.Y); + + // Make the final button sized so that the space between the text and the edges in the X direction is the same as the Y direction. + Point finalButtonSize = new Point(joinButtonTextSize.X + totalAdditionalTextPadding, joinButtonHeight); + + // Add padding to the server name to match the padding on the button text. + serverNameText.Padding = new Vector4(totalAdditionalTextPadding / 2); + + // Get the dimensions of the text we want to show, plus the extra padding we added. + Point serverNameSize = serverNameText.Font.MeasureString(serverNameText.Text).ToPoint() + new Point(totalAdditionalTextPadding, totalAdditionalTextPadding); + + // Now determine how large the parent frame has to be to exactly fit our two controls. + Point frameDims = new Point(serverNameSize.X + finalButtonSize.X + framePadding*2, Math.Max(serverNameSize.Y, finalButtonSize.Y) + framePadding * 2); + + var popupPos = PlayerInput.MousePosition.ToPoint(); + if(popupPos.X+frameDims.X > GUI.Canvas.NonScaledSize.X) + { + // Prevent the Join button from going off the end of the screen if the server name is long or we click a user towards the edge. + popupPos.X = GUI.Canvas.NonScaledSize.X - frameDims.X; + } + + // Apply the size and position changes. + friendPopup.RectTransform.NonScaledSize = frameDims; friendPopup.RectTransform.RelativeOffset = Vector2.Zero; - friendPopup.RectTransform.AbsoluteOffset = PlayerInput.MousePosition.ToPoint(); + friendPopup.RectTransform.AbsoluteOffset = popupPos; + + joinButton.RectTransform.NonScaledSize = finalButtonSize; + friendPopup.RectTransform.RecalculateChildren(true); friendPopup.RectTransform.SetPosition(Anchor.TopLeft); } @@ -2277,13 +2322,29 @@ namespace Barotrauma public override void AddToGUIUpdateList() { menu.AddToGUIUpdateList(); - friendPopup?.AddToGUIUpdateList(); - friendsDropdown?.AddToGUIUpdateList(); - workshopDownloadsFrame?.AddToGUIUpdateList(); } + + public void SaveServerFilters(XElement element) + { + element.RemoveAttributes(); + foreach (KeyValuePair filterBox in filterTickBoxes) + { + element.Add(new XAttribute(filterBox.Key, filterBox.Value.Selected.ToString())); + } + } + + public void LoadServerFilters(XElement element) + { + if (element == null) { return; } + + foreach (KeyValuePair filterBox in filterTickBoxes) + { + filterBox.Value.Selected = element.GetAttributeBool(filterBox.Key, filterBox.Value.Selected); + } + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs index 92af24960..29f734718 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs @@ -351,30 +351,48 @@ namespace Barotrauma void LoadSprites(XElement element) { - element.Elements("sprite").ForEach(s => CreateSprite(s)); - element.Elements("Sprite").ForEach(s => CreateSprite(s)); - element.Elements("deformablesprite").ForEach(s => CreateSprite(s)); - element.Elements("DeformableSprite").ForEach(s => CreateSprite(s)); - element.Elements("backgroundsprite").ForEach(s => CreateSprite(s)); - element.Elements("BackgroundSprite").ForEach(s => CreateSprite(s)); - element.Elements("brokensprite").ForEach(s => CreateSprite(s)); - element.Elements("BrokenSprite").ForEach(s => CreateSprite(s)); - element.Elements("containedsprite").ForEach(s => CreateSprite(s)); - element.Elements("ContainedSprite").ForEach(s => CreateSprite(s)); - element.Elements("inventoryicon").ForEach(s => CreateSprite(s)); - element.Elements("InventoryIcon").ForEach(s => CreateSprite(s)); - element.Elements("icon").ForEach(s => CreateSprite(s)); - element.Elements("Icon").ForEach(s => CreateSprite(s)); - //decorativesprites don't necessarily have textures (can be used to hide/disable other sprites) - element.Elements("decorativesprite").ForEach(s => { if (s.Attribute("texture") != null) CreateSprite(s); }); - element.Elements("DecorativeSprite").ForEach(s => { if (s.Attribute("texture") != null) CreateSprite(s); }); + string[] spriteElementNames = new string[] + { + "Sprite", + "DeformableSprite", + "BackgroundSprite", + "BrokenSprite", + "ContainedSprite", + "InventoryIcon", + "Icon", + "VineSprite", + "LeafSprite", + "FlowerSprite", + "DecorativeSprite" + }; + + foreach (string spriteElementName in spriteElementNames) + { + element.Elements(spriteElementName).ForEach(s => CreateSprite(s)); + element.Elements(spriteElementName.ToLowerInvariant()).ForEach(s => CreateSprite(s)); + } + element.Elements().ForEach(e => LoadSprites(e)); } void CreateSprite(XElement element) { string spriteFolder = ""; - string textureElement = element.GetAttributeString("texture", ""); + string textureElement = ""; + + if (element.Attribute("texture") != null) + { + textureElement = element.GetAttributeString("texture", ""); + } + else + { + if (element.Name.ToString().ToLower() == "vinesprite") + { + textureElement = element.Parent.GetAttributeString("vineatlas", ""); + } + } + if (string.IsNullOrEmpty(textureElement)) { return; } + // TODO: parse and create? if (textureElement.Contains("[GENDER]") || textureElement.Contains("[HEADID]") || textureElement.Contains("[RACE]") || textureElement.Contains("[VARIANT]")) { return; } if (!textureElement.Contains("/")) @@ -388,7 +406,7 @@ namespace Barotrauma //{ // loadedSprites.Add(new Sprite(element, spriteFolder)); //} - loadedSprites.Add(new Sprite(element, spriteFolder)); + loadedSprites.Add(new Sprite(element, spriteFolder, textureElement)); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index a306ae4ff..539bed59b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -129,6 +129,10 @@ namespace Barotrauma public static List SuppressedWarnings = new List(); + public static readonly EditorImageManager ImageManager = new EditorImageManager(); + + public static bool ShouldDrawGrid = false; + //a Character used for picking up and manipulating items private Character dummyCharacter; @@ -173,7 +177,9 @@ namespace Barotrauma private Mode mode; private Color backgroundColor = GameSettings.SubEditorBackgroundColor; - + + private Vector2 MeasurePositionStart = Vector2.Zero; + // Prevent the mode from changing private bool lockMode; @@ -230,7 +236,10 @@ namespace Barotrauma public SubEditorScreen() { - cam = new Camera(); + cam = new Camera + { + MaxZoom = 10f + }; WayPoint.ShowWayPoints = false; WayPoint.ShowSpawnPoints = false; Hull.ShowHulls = false; @@ -535,7 +544,6 @@ namespace Barotrauma lightComponent.Light.Color = item.Container != null || (item.body != null && !item.body.Enabled) ? Color.Transparent : lightComponent.LightColor; - lightComponent.Light.Rotation = (-lightComponent.Rotation - MathHelper.ToRadians(lightComponent.Item.Rotation)); lightComponent.Light.LightSpriteEffect = lightComponent.Item.SpriteEffects; } } @@ -561,6 +569,12 @@ namespace Barotrauma Selected = Item.ShowItems, OnSelected = (GUITickBox obj) => { Item.ShowItems = obj.Selected; return true; } }; + new GUITickBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedShowEntitiesPanel.RectTransform), TextManager.Get("ShowWires")) + { + UserData = "wire", + Selected = Item.ShowWires, + OnSelected = (GUITickBox obj) => { Item.ShowWires = obj.Selected; return true; } + }; new GUITickBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedShowEntitiesPanel.RectTransform), TextManager.Get("ShowWaypoints")) { UserData = "waypoint", @@ -1045,6 +1059,10 @@ namespace Barotrauma if (backedUpSubInfo != null) { Submarine.MainSub = new Submarine(backedUpSubInfo); + if (previewImage != null && backedUpSubInfo.PreviewImage?.Texture != null && !backedUpSubInfo.PreviewImage.Texture.IsDisposed) + { + previewImage.Sprite = backedUpSubInfo.PreviewImage; + } backedUpSubInfo = null; } else if (Submarine.MainSub == null) @@ -1059,10 +1077,12 @@ namespace Barotrauma GameMain.SoundManager.SetCategoryGainMultiplier("default", 0.0f); GameMain.SoundManager.SetCategoryGainMultiplier("waterambience", 0.0f); + string downloadFolder = Path.GetFullPath(SaveUtil.SubmarineDownloadFolder); linkedSubBox.ClearChildren(); foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines) { if (sub.Type != SubmarineType.Player) { continue; } + if (Path.GetDirectoryName(Path.GetFullPath(sub.FilePath)) == downloadFolder) { continue; } linkedSubBox.AddItem(sub.Name, sub); } @@ -1075,6 +1095,8 @@ namespace Barotrauma CoroutineManager.StartCoroutine(AutoSaveCoroutine(), "SubEditorAutoSave"); } + ImageManager.OnEditorSelected(); + GameAnalyticsManager.SetCustomDimension01("editor"); if (!GameMain.Config.EditorDisclaimerShown) { @@ -1089,7 +1111,7 @@ namespace Barotrauma /// private static IEnumerable AutoSaveCoroutine() { - DateTime target = DateTime.Now.AddMinutes(5); + DateTime target = DateTime.Now.AddMinutes(GameSettings.AutoSaveIntervalSeconds); DateTime tempTarget = DateTime.Now; bool wasPaused = false; @@ -1159,7 +1181,20 @@ namespace Barotrauma dummyCharacter = null; GameMain.World.ProcessChanges(); } + + GUIMessageBox.MessageBoxes.ForEachMod(component => + { + if (component is GUIMessageBox { Closed: false, UserData: "colorpicker" } msgBox) + { + foreach (GUIColorPicker colorPicker in msgBox.GetAllChildren()) + { + colorPicker.DisposeTextures(); + } + msgBox.Close(); + } + }); + ClearFilter(); } @@ -1167,7 +1202,7 @@ namespace Barotrauma { if (dummyCharacter != null) RemoveDummyCharacter(); - dummyCharacter = Character.Create(CharacterPrefab.HumanSpeciesName, Vector2.Zero, "", id: Entity.RespawnManagerID, hasAi: false); + dummyCharacter = Character.Create(CharacterPrefab.HumanSpeciesName, Vector2.Zero, "", id: Entity.DummyID, hasAi: false); dummyCharacter.Info.Name = "Galldren"; //make space for the entity menu @@ -1485,7 +1520,7 @@ namespace Barotrauma if (Submarine.MainSub != null) { Barotrauma.IO.Validation.DevException = true; - if (previewImage?.Sprite?.Texture != null && Submarine.MainSub.Info.Type != SubmarineType.OutpostModule) + if (previewImage?.Sprite?.Texture != null && !previewImage.Sprite.Texture.IsDisposed && Submarine.MainSub.Info.Type != SubmarineType.OutpostModule) { bool savePreviewImage = true; using System.IO.MemoryStream imgStream = new System.IO.MemoryStream(); @@ -1513,10 +1548,12 @@ namespace Barotrauma SubmarineInfo.RefreshSavedSub(savePath); if (prevSavePath != null && prevSavePath != savePath) { SubmarineInfo.RefreshSavedSub(prevSavePath); } + string downloadFolder = Path.GetFullPath(SaveUtil.SubmarineDownloadFolder); linkedSubBox.ClearChildren(); foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines) { if (sub.Type != SubmarineType.Player) { continue; } + if (Path.GetDirectoryName(Path.GetFullPath(sub.FilePath)) == downloadFolder) { continue; } linkedSubBox.AddItem(sub.Name, sub); } subNameLabel.Text = ToolBox.LimitString(Submarine.MainSub.Info.Name, subNameLabel.Font, subNameLabel.Rect.Width); @@ -1719,7 +1756,7 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), locationTypeGroup.RectTransform), TextManager.Get("outpostmoduleallowedlocationtypes"), textAlignment: Alignment.CenterLeft); HashSet availableLocationTypes = new HashSet { "any" }; - foreach (LocationType locationType in LocationType.List) { availableLocationTypes.Add(locationType.Identifier); } + foreach (LocationType locationType in LocationType.List) { availableLocationTypes.Add(locationType.Identifier.ToLowerInvariant()); } var locationTypeDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), locationTypeGroup.RectTransform), text: string.Join(", ", Submarine.MainSub?.Info?.OutpostModuleInfo?.AllowedLocationTypes.Select(lt => TextManager.Capitalize(lt)) ?? "any".ToEnumerable()), selectMultiple: true); @@ -1814,7 +1851,7 @@ namespace Barotrauma }; new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), commonnessGroup.RectTransform), TextManager.Get("subeditor.outpostcommonness"), textAlignment: Alignment.CenterLeft, wrap: true); - new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), commonnessGroup.RectTransform), GUINumberInput.NumberType.Int) + new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), commonnessGroup.RectTransform), GUINumberInput.NumberType.Float) { FloatValue = Submarine.MainSub?.Info?.OutpostModuleInfo?.Commonness ?? 10, MinValueFloat = 0, @@ -2349,7 +2386,8 @@ namespace Barotrauma searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; searchBox.OnTextChanged += (textBox, text) => { FilterSubs(subList, text); return true; }; - List sortedSubs = new List(SubmarineInfo.SavedSubmarines); + string downloadFolder = Path.GetFullPath(SaveUtil.SubmarineDownloadFolder); + List sortedSubs = new List(SubmarineInfo.SavedSubmarines.Where(s => Path.GetDirectoryName(Path.GetFullPath(s.FilePath)) != downloadFolder)); sortedSubs.Sort((s1, s2) => { return s1.Type.CompareTo(s2.Type) * 100 + s1.Name.CompareTo(s2.Name); }); SubmarineInfo prevSub = null; @@ -2759,11 +2797,7 @@ namespace Barotrauma { if (dummyCharacter == null || dummyCharacter.Removed) { return; } - foreach (Item item in dummyCharacter.Inventory.Items) - { - item?.Remove(); - } - + dummyCharacter.Inventory.AllItems.ForEachMod(it => it.Remove()); dummyCharacter.Remove(); dummyCharacter = null; } @@ -2806,6 +2840,11 @@ namespace Barotrauma { UserData = "transparency" }; + new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), + TextManager.Get("SubEditor.ToggleGrid"), font: GUI.SmallFont) + { + UserData = "togglegrid" + }; new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), TextManager.Get("editor.selectsame"), font: GUI.SmallFont) @@ -2813,6 +2852,18 @@ namespace Barotrauma UserData = "selectsame", Enabled = targets.Count > 0 }; + + new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), + TextManager.Get("SubEditor.AddImage"), font: GUI.SmallFont) + { + UserData = "addimage" + }; + + new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), + TextManager.Get("SubEditor.ToggleImageEditing"), font: GUI.SmallFont) + { + UserData = "editimages" + }; } else { @@ -2882,11 +2933,21 @@ namespace Barotrauma case "bgcolor": CreateBackgroundColorPicker(); break; + case "togglegrid": + ShouldDrawGrid = !ShouldDrawGrid; + break; + case "addimage": + ImageManager.CreateImageWizard(PlayerInput.MousePosition); + break; + case "editimages": + ImageManager.EditorMode = !ImageManager.EditorMode; + if (!ImageManager.EditorMode) { GameMain.Config.SaveNewPlayerConfig(); } + break; case "transparency": TransparentWiringMode = !TransparentWiringMode; break; case "selectsame": - IEnumerable matching = MapEntity.mapEntityList.Where(e => e.prefab != null && targets.Any(t => t.prefab.Identifier == e.prefab.Identifier) && !MapEntity.SelectedList.Contains(e)); + 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); break; case "copy": @@ -2911,6 +2972,214 @@ namespace Barotrauma }; } + public static GUIMessageBox CreatePropertyColorPicker(Color originalColor, SerializableProperty property, ISerializableEntity entity) + { + bool setValues = true; + object sliderMutex = new object(), + sliderTextMutex = new object(), + pickerMutex = new object(), + hexMutex = new object(); + + Vector2 relativeSize = new Vector2(GUI.IsFourByThree() ? 0.4f : 0.3f, 0.3f); + + GUIMessageBox msgBox = new GUIMessageBox(string.Empty, string.Empty, Array.Empty(), relativeSize, type: GUIMessageBox.Type.Vote) + { + UserData = "colorpicker", + Draggable = true + }; + + GUILayoutGroup contentLayout = new GUILayoutGroup(new RectTransform(Vector2.One, msgBox.Content.RectTransform)); + GUITextBlock headerText = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), contentLayout.RectTransform), property.Name, font: GUI.SubHeadingFont, textAlignment: Alignment.TopCenter) + { + AutoScaleVertical = true + }; + + GUILayoutGroup colorLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.7f), contentLayout.RectTransform), isHorizontal: true); + + GUILayoutGroup buttonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.2f), contentLayout.RectTransform), childAnchor: Anchor.BottomLeft, isHorizontal: true) + { + RelativeSpacing = 0.1f, + Stretch = true + }; + + GUIButton closeButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1f), buttonLayout.RectTransform), TextManager.Get("OK"), textAlignment: Alignment.Center); + GUIButton cancelButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1f), buttonLayout.RectTransform), TextManager.Get("Cancel"), textAlignment: Alignment.Center); + + contentLayout.Recalculate(); + colorLayout.Recalculate(); + + GUIColorPicker colorPicker = new GUIColorPicker(new RectTransform(new Point(colorLayout.Rect.Height), colorLayout.RectTransform)); + var (h, s, v) = ToolBox.RGBToHSV(originalColor); + colorPicker.SelectedHue = float.IsNaN(h) ? 0f : h; + colorPicker.SelectedSaturation = s; + colorPicker.SelectedValue = v; + + colorLayout.Recalculate(); + + GUILayoutGroup sliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f - colorPicker.RectTransform.RelativeSize.X, 1f), colorLayout.RectTransform), childAnchor: Anchor.TopRight); + + float currentHue = colorPicker.SelectedHue / 360f; + GUILayoutGroup hueSliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.25f), sliderLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + new GUITextBlock(new RectTransform(new Vector2(0.1f, 0.2f), hueSliderLayout.RectTransform), text: "H:", font: GUI.SubHeadingFont) { Padding = Vector4.Zero, ToolTip = "Hue" }; + GUIScrollBar hueScrollBar = new GUIScrollBar(new RectTransform(new Vector2(0.7f, 1f), hueSliderLayout.RectTransform), style: "GUISlider", barSize: 0.05f) { BarScroll = currentHue }; + GUINumberInput hueTextBox = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1f), hueSliderLayout.RectTransform), inputType: GUINumberInput.NumberType.Float) { FloatValue = currentHue, MaxValueFloat = 1f, MinValueFloat = 0f, DecimalsToDisplay = 2 }; + + GUILayoutGroup satSliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.2f), sliderLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + new GUITextBlock(new RectTransform(new Vector2(0.1f, 0.2f), satSliderLayout.RectTransform), text: "S:", font: GUI.SubHeadingFont) { Padding = Vector4.Zero, ToolTip = "Saturation"}; + GUIScrollBar satScrollBar = new GUIScrollBar(new RectTransform(new Vector2(0.7f, 1f), satSliderLayout.RectTransform), style: "GUISlider", barSize: 0.05f) { BarScroll = colorPicker.SelectedSaturation }; + GUINumberInput satTextBox = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1f), satSliderLayout.RectTransform), inputType: GUINumberInput.NumberType.Float) { FloatValue = colorPicker.SelectedSaturation, MaxValueFloat = 1f, MinValueFloat = 0f, DecimalsToDisplay = 2 }; + + GUILayoutGroup valueSliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.2f), sliderLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + new GUITextBlock(new RectTransform(new Vector2(0.1f, 0.2f), valueSliderLayout.RectTransform), text: "V:", font: GUI.SubHeadingFont) { Padding = Vector4.Zero, ToolTip = "Value"}; + GUIScrollBar valueScrollBar = new GUIScrollBar(new RectTransform(new Vector2(0.7f, 1f), valueSliderLayout.RectTransform), style: "GUISlider", barSize: 0.05f) { BarScroll = colorPicker.SelectedValue }; + GUINumberInput valueTextBox = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1f), valueSliderLayout.RectTransform), inputType: GUINumberInput.NumberType.Float) { FloatValue = colorPicker.SelectedValue, MaxValueFloat = 1f, MinValueFloat = 0f, DecimalsToDisplay = 2 }; + + GUILayoutGroup colorInfoLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.3f), sliderLayout.RectTransform), childAnchor: Anchor.CenterLeft, isHorizontal: true) { RelativeSpacing = 0.15f }; + + new GUICustomComponent(new RectTransform(new Vector2(0.4f, 0.8f), colorInfoLayout.RectTransform), (batch, component) => + { + Rectangle rect = component.Rect; + Point areaSize = new Point(rect.Width, rect.Height / 2); + Rectangle newColorRect = new Rectangle(rect.Location, areaSize); + Rectangle oldColorRect = new Rectangle(new Point(newColorRect.Left, newColorRect.Bottom), areaSize); + + GUI.DrawRectangle(batch, newColorRect, ToolBox.HSVToRGB(colorPicker.SelectedHue, colorPicker.SelectedSaturation, colorPicker.SelectedValue), isFilled: true); + GUI.DrawRectangle(batch, oldColorRect, originalColor, isFilled: true); + GUI.DrawRectangle(batch, rect, Color.Black, isFilled: false); + }); + + GUITextBox hexValueBox = new GUITextBox(new RectTransform(new Vector2(0.3f, 1f), colorInfoLayout.RectTransform), text: ColorToHex(originalColor), createPenIcon: false) { OverflowClip = true }; + + hueScrollBar.OnMoved = (bar, scroll) => { SetColor(sliderMutex); return true; }; + hueTextBox.OnValueChanged = input => { SetColor(sliderTextMutex); }; + + satScrollBar.OnMoved = (bar, scroll) => { SetColor(sliderMutex); return true; }; + satTextBox.OnValueChanged = input => { SetColor(sliderTextMutex); }; + + valueScrollBar.OnMoved = (bar, scroll) => { SetColor(sliderMutex); return true; }; + valueTextBox.OnValueChanged = input => { SetColor(sliderTextMutex); }; + + colorPicker.OnColorSelected = (component, color) => { SetColor(pickerMutex); return true; }; + + hexValueBox.OnEnterPressed = (box, text) => { SetColor(hexMutex); return true; }; + hexValueBox.OnDeselected += (sender, key) => { SetColor(hexMutex); }; + + closeButton.OnClicked = (button, o) => + { + colorPicker.DisposeTextures(); + msgBox.Close(); + + if (entity is MapEntity { Removed: true } me) { return true; } + Color newColor = SetColor(null); + StoreCommand(new PropertyCommand(entity, property.Name, newColor, originalColor)); + + if (MapEntity.EditingHUD != null && (MapEntity.EditingHUD.UserData == entity || (!(entity is ItemComponent ic) || MapEntity.EditingHUD.UserData == ic.Item))) + { + GUIListBox list = MapEntity.EditingHUD.GetChild(); + if (list != null) + { + IEnumerable editors = list.Content.FindChildren(comp => comp is SerializableEntityEditor).Cast(); + SerializableEntityEditor.LockEditing = true; + foreach (SerializableEntityEditor editor in editors) + { + if (editor.UserData == entity && editor.Fields.TryGetValue(property.Name, out GUIComponent[] _)) + { + editor.UpdateValue(property, newColor, flash: false); + } + } + SerializableEntityEditor.LockEditing = false; + } + } + return true; + }; + + cancelButton.OnClicked = (button, o) => + { + colorPicker.DisposeTextures(); + msgBox.Close(); + if (entity is MapEntity { Removed: true } me) { return true; } + property.SetValue(entity, originalColor); + return true; + }; + + return msgBox; + + Color SetColor(object source) + { + if (setValues) + { + setValues = false; + + if (source == sliderMutex) + { + Vector3 hsv = new Vector3(hueScrollBar.BarScroll * 360f, satScrollBar.BarScroll, valueScrollBar.BarScroll); + SetSliderTexts(hsv); + SetColorPicker(hsv); + SetHex(hsv); + } + else if (source == sliderTextMutex) + { + Vector3 hsv = new Vector3(hueTextBox.FloatValue * 360f, satTextBox.FloatValue, valueTextBox.FloatValue); + SetSliders(hsv); + SetColorPicker(hsv); + SetHex(hsv); + } + else if (source == pickerMutex) + { + Vector3 hsv = new Vector3(colorPicker.SelectedHue, colorPicker.SelectedSaturation, colorPicker.SelectedValue); + SetSliders(hsv); + SetSliderTexts(hsv); + SetHex(hsv); + } + else if (source == hexMutex) + { + Vector3 hsv = ToolBox.RGBToHSV(XMLExtensions.ParseColor(hexValueBox.Text, errorMessages: false)); + if (float.IsNaN(hsv.X)) { hsv.X = 0f; } + SetSliders(hsv); + SetSliderTexts(hsv); + SetColorPicker(hsv); + SetHex(hsv); + } + + setValues = true; + } + + Color color = ToolBox.HSVToRGB(colorPicker.SelectedHue, colorPicker.SelectedSaturation, colorPicker.SelectedValue); + color.A = originalColor.A; + property.TrySetValue(entity, color); + return color; + + void SetSliders(Vector3 hsv) + { + hueScrollBar.BarScroll = hsv.X / 360f; + satScrollBar.BarScroll = hsv.Y; + valueScrollBar.BarScroll = hsv.Z; + } + + void SetSliderTexts(Vector3 hsv) + { + hueTextBox.FloatValue = hsv.X / 360f; + satTextBox.FloatValue = hsv.Y; + valueTextBox.FloatValue = hsv.Z; + } + + void SetColorPicker(Vector3 hsv) + { + colorPicker.SelectedHue = hsv.X; + colorPicker.SelectedSaturation = hsv.Y; + colorPicker.SelectedValue = hsv.Z; + } + + void SetHex(Vector3 hsv) + { + Color hexColor = ToolBox.HSVToRGB(hsv.X, hsv.Y, hsv.Z); + hexValueBox!.Text = ColorToHex(hexColor); + } + } + + static string ColorToHex(Color color) => $"#{(color.R << 16 | color.G << 8 | color.B):X6}"; + } + /// /// Creates a color picker that can be used to change the submarine editor's background color /// @@ -3005,7 +3274,7 @@ namespace Barotrauma if (dummyCharacter == null) return false; //if the same type of wire has already been selected, deselect it and return - Item existingWire = dummyCharacter.SelectedItems.FirstOrDefault(i => i != null && i.Prefab == userData as ItemPrefab); + Item existingWire = dummyCharacter.HeldItems.FirstOrDefault(i => i.Prefab == userData as ItemPrefab); if (existingWire != null) { existingWire.Drop(null); @@ -3018,7 +3287,7 @@ namespace Barotrauma int slotIndex = dummyCharacter.Inventory.FindLimbSlot(InvSlotType.LeftHand); //if there's some other type of wire in the inventory, remove it - existingWire = dummyCharacter.Inventory.Items[slotIndex]; + existingWire = dummyCharacter.Inventory.GetItemAt(slotIndex); if (existingWire != null && existingWire.Prefab != userData as ItemPrefab) { existingWire.Drop(null); @@ -3749,6 +4018,8 @@ namespace Barotrauma /// public override void Update(double deltaTime) { + ImageManager.Update((float) deltaTime); + if (GameMain.GraphicsWidth != screenResolution.X || GameMain.GraphicsHeight != screenResolution.Y) { saveFrame = null; @@ -3760,9 +4031,8 @@ namespace Barotrauma if (WiringMode && dummyCharacter != null) { - Wire equippedWire = - Character.Controlled?.SelectedItems[0]?.GetComponent() ?? - Character.Controlled?.SelectedItems[1]?.GetComponent() ?? + Wire equippedWire = + Character.Controlled?.HeldItems.FirstOrDefault(it => it.GetComponent() != null)?.GetComponent() ?? Wire.DraggingWire; if (equippedWire == null) @@ -3849,6 +4119,25 @@ namespace Barotrauma } } + if (WiringMode && dummyCharacter != null) + { + if (wiringToolPanel.GetChild() is { } listBox) + { + if (!dummyCharacter.HeldItems.Any(it => it.HasTag("wire"))) + { + listBox.Deselect(); + } + + List numberKeys = PlayerInput.NumberKeys; + if (numberKeys.Find(PlayerInput.KeyHit) is { } key) + { + // treat 0 as the last key instead of first + int index = key == Keys.D0 ? numberKeys.Count : numberKeys.IndexOf(key) - 1; + listBox.Select(index, force: false, autoScroll: true, takeKeyBoardFocus: false); + } + } + } + if (GUI.KeyboardDispatcher.Subscriber == null) { if (PlayerInput.KeyHit(Keys.E) && mode == Mode.Default) @@ -4012,7 +4301,7 @@ namespace Barotrauma { // Move all of our slots on top center of the entity list // We use the slots to open item inventories and we want the position of them to be consisent - dummyCharacter.Inventory.slots.ForEach(slot => + dummyCharacter.Inventory.visualSlots.ForEach(slot => { slot.Rect.Y = EntityMenu.Rect.Top; slot.Rect.X = EntityMenu.Rect.X + (EntityMenu.Rect.Width / 2) - (slot.Rect.Width /2); @@ -4024,9 +4313,7 @@ namespace Barotrauma { if (WiringMode && PlayerInput.IsShiftDown()) { - Wire equippedWire = - Character.Controlled?.SelectedItems[0]?.GetComponent() ?? - Character.Controlled?.SelectedItems[1]?.GetComponent(); + Wire equippedWire = Character.Controlled?.HeldItems.FirstOrDefault(i => i.GetComponent() != null)?.GetComponent(); if (equippedWire != null && equippedWire.GetNodes().Count > 0) { Vector2 lastNode = equippedWire.GetNodes().Last(); @@ -4072,12 +4359,12 @@ namespace Barotrauma // Deposit item from our "infinite stack" into inventory slots var inv = dummyCharacter?.SelectedConstruction?.OwnInventory; - if (inv?.slots != null && !PlayerInput.IsCtrlDown()) + if (inv?.visualSlots != null && !PlayerInput.IsCtrlDown()) { var dragginMouse = MouseDragStart != Vector2.Zero && Vector2.Distance(PlayerInput.MousePosition, MouseDragStart) >= GUI.Scale * 20; // So we don't accidentally drag inventory items while doing this - if (DraggedItemPrefab != null) { Inventory.draggingItem = null; } + if (DraggedItemPrefab != null) { Inventory.DraggingItems.Clear(); } switch (DraggedItemPrefab) { @@ -4085,17 +4372,17 @@ namespace Barotrauma case ItemPrefab itemPrefab when PlayerInput.PrimaryMouseButtonClicked() || dragginMouse: { bool spawnedItem = false; - for (var i = 0; i < inv.slots.Length; i++) + for (var i = 0; i < inv.Capacity; i++) { - var slot = inv.slots[i]; - var itemContainer = inv?.Items[i]?.GetComponent(); + var slot = inv.visualSlots[i]; + var itemContainer = inv.GetItemAt(i)?.GetComponent(); // check if the slot is empty or if we can place the item into a container, for example an oxygen tank into a diving suit if (Inventory.IsMouseOnSlot(slot)) { var newItem = new Item(itemPrefab, Vector2.Zero, Submarine.MainSub); - if (inv.Items[i] == null) + if (inv.CanBePut(itemPrefab, i)) { bool placedItem = inv.TryPutItem(newItem, i, false, true, dummyCharacter); spawnedItem |= placedItem; @@ -4105,8 +4392,7 @@ namespace Barotrauma newItem.Remove(); } } - else if (itemContainer != null && itemContainer.CanBeContained(itemPrefab) && - (itemContainer.Inventory?.Items.Any(item => item == null) ?? false)) + else if (itemContainer != null && itemContainer.Inventory.CanBePut(itemPrefab)) { bool placedItem = itemContainer.Inventory.TryPutItem(newItem, dummyCharacter); spawnedItem |= placedItem; @@ -4124,6 +4410,7 @@ namespace Barotrauma else { newItem.Remove(); + slot.ShowBorderHighlight(GUI.Style.Red, 0.1f, 0.4f); } if (!newItem.Removed) @@ -4144,11 +4431,12 @@ namespace Barotrauma case ItemAssemblyPrefab assemblyPrefab when PlayerInput.PrimaryMouseButtonClicked(): { bool spawnedItems = false; - for (var i = 0; i < inv.slots.Length; i++) + for (var i = 0; i < inv.visualSlots.Length; i++) { - var slot = inv.slots[i]; - var itemContainer = inv?.Items[i]?.GetComponent(); - if (inv.Items[i] == null && Inventory.IsMouseOnSlot(slot)) + var slot = inv.visualSlots[i]; + var item = inv?.GetItemAt(i); + var itemContainer = item?.GetComponent(); + if (item == null && Inventory.IsMouseOnSlot(slot)) { // load the items var itemInstance = LoadItemAssemblyInventorySafe(assemblyPrefab); @@ -4162,14 +4450,14 @@ namespace Barotrauma var newSpot = i + j - failedCount; // try to find a valid slot to put the items - while (inv.slots.Length > newSpot) + while (inv.visualSlots.Length > newSpot) { - if (inv.Items[newSpot] == null) { break; } + if (inv.GetItemAt(newSpot) == null) { break; } newSpot++; } // valid slot found - if (inv.slots.Length > newSpot) + if (inv.visualSlots.Length > newSpot) { var placedItem = inv.TryPutItem(newItem, newSpot, false, true, dummyCharacter); spawnedItems |= placedItem; @@ -4239,6 +4527,19 @@ namespace Barotrauma { MapEntity.UpdateSelecting(cam); } + + if (!PlayerInput.PrimaryMouseButtonHeld()) + { + MeasurePositionStart = Vector2.Zero; + } + + if (PlayerInput.KeyDown(Keys.LeftAlt) || PlayerInput.KeyDown(Keys.RightAlt)) + { + if (PlayerInput.PrimaryMouseButtonDown()) + { + MeasurePositionStart = cam.ScreenToWorld(PlayerInput.MousePosition); + } + } if (!WiringMode) { @@ -4280,14 +4581,6 @@ namespace Barotrauma EntityMenu.RectTransform.ScreenSpaceOffset = Vector2.Lerp(new Vector2(0.0f, EntityMenu.Rect.Height - 10), Vector2.Zero, entityMenuOpenState).ToPoint(); - if (WiringMode && dummyCharacter != null) - { - if (!dummyCharacter.SelectedItems.Any(it => it != null && it.HasTag("wire"))) - { - wiringToolPanel.GetChild().Deselect(); - } - } - if (PlayerInput.PrimaryMouseButtonClicked() && !GUI.IsMouseOn(entityFilterBox)) { entityFilterBox.Deselect(); @@ -4312,10 +4605,8 @@ namespace Barotrauma { dummyCharacter.AnimController.FindHull(dummyCharacter.CursorWorldPosition, false); - foreach (Item item in dummyCharacter.Inventory.Items) + foreach (Item item in dummyCharacter.Inventory.AllItems) { - if (item == null) continue; - item.SetTransform(dummyCharacter.SimPosition, 0.0f); item.UpdateTransform(); item.SetTransform(item.body.SimPosition, 0.0f); @@ -4363,8 +4654,11 @@ namespace Barotrauma sub.UpdateTransform(); } - spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, transformMatrix: cam.Transform); graphics.Clear(backgroundColor); + ImageManager.Draw(spriteBatch, cam); + + spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, transformMatrix: cam.Transform); + if (GameMain.DebugDraw) { GUI.DrawLine(spriteBatch, new Vector2(Submarine.MainSub.HiddenSubPosition.X, -cam.WorldView.Y), new Vector2(Submarine.MainSub.HiddenSubPosition.X, -(cam.WorldView.Y - cam.WorldView.Height)), Color.White * 0.5f, 1.0f, (int)(2.0f / cam.Zoom)); @@ -4409,15 +4703,17 @@ namespace Barotrauma } if (dummyCharacter != null && WiringMode) { - for (int i = 0; i < dummyCharacter.SelectedItems.Length; i++) + foreach (Item heldItem in dummyCharacter.HeldItems) { - if (dummyCharacter.SelectedItems[i] == null) { continue; } - if (i > 0 && dummyCharacter.SelectedItems[0] == dummyCharacter.SelectedItems[i]) { continue; } - dummyCharacter.SelectedItems[i].Draw(spriteBatch, editing: false, back: true); + heldItem.Draw(spriteBatch, editing: false, back: true); } } + + DrawGrid(spriteBatch); spriteBatch.End(); + ImageManager.DrawEditing(spriteBatch, cam); + if (GameMain.LightManager.LightingEnabled && lightingEnabled) { spriteBatch.Begin(SpriteSortMode.Deferred, Lights.CustomBlendStates.Multiplicative, null, DepthStencilState.None); @@ -4429,7 +4725,7 @@ namespace Barotrauma spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState); - if (Submarine.MainSub != null) + if (Submarine.MainSub != null && cam.Zoom < 5f) { Vector2 position = Submarine.MainSub.SubBody != null ? Submarine.MainSub.WorldPosition : Submarine.MainSub.HiddenSubPosition; @@ -4467,6 +4763,31 @@ namespace Barotrauma MapEntity.DrawEditor(spriteBatch, cam); GUI.Draw(Cam, spriteBatch); + + if (MeasurePositionStart != Vector2.Zero) + { + Vector2 startPos = MeasurePositionStart; + Vector2 mouseWorldPos = cam.ScreenToWorld(PlayerInput.MousePosition); + if (PlayerInput.IsShiftDown()) + { + startPos = RoundToGrid(startPos); + mouseWorldPos = RoundToGrid(mouseWorldPos); + + static Vector2 RoundToGrid(Vector2 position) + { + position.X = (float) Math.Round(position.X / Submarine.GridSize.X) * Submarine.GridSize.X; + position.Y = (float) Math.Round(position.Y / Submarine.GridSize.Y) * Submarine.GridSize.Y; + return position; + } + } + + GUI.DrawLine(spriteBatch, cam.WorldToScreen(startPos), cam.WorldToScreen(mouseWorldPos), GUI.Style.Green, width: 4); + + decimal realWorldDistance = decimal.Round((decimal) (Vector2.Distance(startPos, mouseWorldPos) * Physics.DisplayToRealWorldRatio), 2); + + Vector2 offset = new Vector2(GUI.IntScale(24)); + GUI.DrawString(spriteBatch, PlayerInput.MousePosition + offset, $"{realWorldDistance}m", GUI.Style.TextColor, font: GUI.SubHeadingFont, backgroundColor: Color.Black, backgroundPadding: 4); + } spriteBatch.End(); } @@ -4515,6 +4836,42 @@ namespace Barotrauma GameMain.Instance.ResetViewPort(); } + private static readonly Color gridBaseColor = Color.White * 0.1f; + + private void DrawGrid(SpriteBatch spriteBatch) + { + // don't render at high zoom levels because it would just turn the screen white + if (cam.Zoom < 0.5f || !ShouldDrawGrid) { return; } + + var (gridX, gridY) = Submarine.GridSize; + + int scale = Math.Max(1, GUI.IntScale(1)); + float zoom = cam.Zoom / 2f; // Don't ask + float lineThickness = Math.Max(1, scale / zoom); + + Color gridColor = gridBaseColor; + if (cam.Zoom < 1.0f) + { + // fade the grid when zooming out + gridColor *= Math.Max(0, (cam.Zoom - 0.5f) * 2f); + } + + Rectangle camRect = cam.WorldView; + + for (float x = snapX(camRect.X); x < snapX(camRect.X + camRect.Width) + gridX; x += gridX) + { + spriteBatch.DrawLine(new Vector2(x, -camRect.Y), new Vector2(x, -(camRect.Y - camRect.Height)), gridColor, thickness: lineThickness); + } + + for (float y = snapY(camRect.Y); y >= snapY(camRect.Y - camRect.Height) - gridY; y -= Submarine.GridSize.Y) + { + spriteBatch.DrawLine(new Vector2(camRect.X, -y), new Vector2(camRect.Right, -y), gridColor, thickness: lineThickness); + } + + float snapX(int x) => (float) Math.Floor(x / gridX) * gridX; + float snapY(int y) => (float) Math.Ceiling(y / gridY) * gridY; + } + public void SaveScreenShot(int width, int height, string filePath) { System.IO.Stream stream = File.OpenWrite(filePath); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index 1bd280e4f..3b9d3d2d1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -228,6 +228,14 @@ namespace Barotrauma } } } + + if (fields.FirstOrDefault() is { } comp && comp.Parent?.Parent?.Parent is { } parent) + { + if (parent.FindChild("colorpreview", true) is GUIButton preview) + { + preview.Color = preview.HoverColor = preview.PressedColor = preview.SelectedTextColor = c; + } + } } else if (newValue is Rectangle r) { @@ -927,7 +935,18 @@ namespace Barotrauma { AbsoluteOffset = new Point(label.Rect.Width, 0) }, color: Color.Black, style: null); - var colorBox = new GUIFrame(new RectTransform(new Vector2(largeInputFieldWidth, 0.9f), colorBoxBack.RectTransform, Anchor.Center), style: null); + var colorBox = new GUIButton(new RectTransform(new Vector2(largeInputFieldWidth, 0.9f), colorBoxBack.RectTransform, Anchor.Center), style: null) + { + UserData = "colorpreview", + OnClicked = (component, data) => + { + if (!SubEditorScreen.IsSubEditor()) { return false; } + if (GUIMessageBox.MessageBoxes.Any(msgBox => msgBox is GUIMessageBox { Closed: false, UserData: "colorpicker" })) { return false; } + + GUIMessageBox msgBox = SubEditorScreen.CreatePropertyColorPicker((Color) property.GetValue(entity), property, entity); + return true; + } + }; var inputArea = new GUILayoutGroup(new RectTransform(new Vector2(Math.Max((frame.Rect.Width - label.Rect.Width - colorBoxBack.Rect.Width) / (float)frame.Rect.Width, 0.5f), 1), frame.RectTransform, Anchor.TopRight), isHorizontal: true, childAnchor: Anchor.CenterRight) { Stretch = true, @@ -976,10 +995,10 @@ namespace Barotrauma if (SetPropertyValue(property, entity, newVal)) { TrySendNetworkUpdate(entity, property); - colorBox.Color = newVal; + colorBox.Color = colorBox.HoverColor = colorBox.PressedColor = colorBox.SelectedTextColor = newVal; } }; - colorBox.Color = (Color)property.GetValue(entity); + colorBox.Color = colorBox.HoverColor = colorBox.PressedColor = colorBox.SelectedTextColor = (Color)property.GetValue(entity); fields[i] = numberInput; } frame.RectTransform.MinSize = new Point(0, frame.RectTransform.Children.Max(c => c.MinSize.Y)); @@ -1111,7 +1130,7 @@ namespace Barotrauma List entities = new List { sEntity }; Dictionary affected = MultiSetProperties(property, entity, value); - Dictionary> oldValues = new Dictionary> {{ oldData, new List { sEntity }}}; + Dictionary> oldValues = new Dictionary> {{ oldData!, new List { sEntity }}}; affected.ForEach(aEntity => { @@ -1186,7 +1205,7 @@ namespace Barotrauma case Item _: if (entity.GetType() == parentObject.GetType()) { - affected.Add((ISerializableEntity) entity, property.GetValue(entity)); + SafeAdd((ISerializableEntity) entity, property); property.PropertyInfo.SetValue(entity, value); } else if (entity is ISerializableEntity sEntity && sEntity.SerializableProperties != null) @@ -1195,7 +1214,7 @@ namespace Barotrauma if (props.TryGetValue(property.NameToLowerInvariant, out SerializableProperty foundProp)) { - affected.Add(sEntity, foundProp.GetValue(sEntity)); + SafeAdd(sEntity, foundProp); foundProp.PropertyInfo.SetValue(entity, value); } } @@ -1205,7 +1224,7 @@ namespace Barotrauma { if (component.GetType() == parentObject.GetType() && component != parentObject) { - affected.Add(component, property.GetValue(component)); + SafeAdd(component, property); property.PropertyInfo.SetValue(component, value); } } @@ -1214,6 +1233,13 @@ namespace Barotrauma } return affected; + + void SafeAdd(ISerializableEntity entity, SerializableProperty prop) + { + object obj = prop.GetValue(entity); + if (prop.PropertyType == typeof(string) && obj == null) { obj = string.Empty; } + affected.Add(entity, obj); + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs index fb85c55a1..8aadfa00b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs @@ -76,8 +76,6 @@ namespace Barotrauma.Sounds protected set; } - public bool IgnoreMuffling { get; set; } - /// /// How many instances of the same sound clip can be playing at the same time /// diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs index 03420baa7..05d332d20 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs @@ -561,13 +561,13 @@ namespace Barotrauma.Sounds } } - public void SetCategoryMuffle(string category,bool muffle) + public void SetCategoryMuffle(string category, bool muffle) { if (Disabled) { return; } category = category.ToLower(); - if (categoryModifiers == null) categoryModifiers = new Dictionary(); + if (categoryModifiers == null) { categoryModifiers = new Dictionary(); } if (!categoryModifiers.ContainsKey(category)) { categoryModifiers.Add(category, new CategoryModifier(0, 1.0f, muffle)); @@ -585,7 +585,7 @@ namespace Barotrauma.Sounds { if (playingChannels[i][j] != null && playingChannels[i][j].IsPlaying) { - if (playingChannels[i][j].Category.ToLower() == category) playingChannels[i][j].Muffled = muffle; + if (playingChannels[i][j]?.Category.ToLower() == category) { playingChannels[i][j].Muffled = muffle; } } } } @@ -597,7 +597,7 @@ namespace Barotrauma.Sounds if (Disabled) { return false; } category = category.ToLower(); - if (categoryModifiers == null || !categoryModifiers.ContainsKey(category)) return false; + if (categoryModifiers == null || !categoryModifiers.ContainsKey(category)) { return false; } return categoryModifiers[category].Muffle; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs index 8f1594a4e..ffbee3767 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs @@ -411,7 +411,11 @@ namespace Barotrauma if (animController.HeadInWater) { ambienceVolume = 1.0f; - ambienceVolume += animController.Limbs[0].LinearVelocity.Length(); + float limbSpeed = animController.Limbs[0].LinearVelocity.Length(); + if (MathUtils.IsValid(limbSpeed)) + { + ambienceVolume += limbSpeed; + } } } @@ -455,6 +459,13 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("SoundPlayer.UpdateWaterAmbience:InvalidVolume", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); movementSoundVolume = 0.0f; } + if (!MathUtils.IsValid(insideSubFactor)) + { + string errorMsg = "Failed to update water ambience volume - inside sub value invalid (" + insideSubFactor + ")"; + DebugConsole.Log(errorMsg); + GameAnalyticsManager.AddErrorEventOnce("SoundPlayer.UpdateWaterAmbience:InvalidVolume", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + insideSubFactor = 0.0f; + } } for (int i = 0; i < 3; i++) @@ -477,9 +488,10 @@ namespace Barotrauma break; } - // Consider the volume set in sounds.xml - if (sound != null) { volume *= sound.BaseGain; } + if (sound == null) { continue; } + // Consider the volume set in sounds.xml + volume *= sound.BaseGain; if ((waterAmbienceChannels[i] == null || !waterAmbienceChannels[i].IsPlaying) && volume > 0.01f) { waterAmbienceChannels[i] = sound.Play(volume, "waterambience"); @@ -759,7 +771,7 @@ namespace Barotrauma return PlaySound(sound, position, volume ?? sound.BaseGain, range ?? sound.BaseFar, 1.0f, hullGuess); } - public static SoundChannel PlaySound(Sound sound, Vector2 position, float? volume = null, float? range = null, float? freqMult = null, Hull hullGuess = null) + public static SoundChannel PlaySound(Sound sound, Vector2 position, float? volume = null, float? range = null, float? freqMult = null, Hull hullGuess = null, bool ignoreMuffling = false) { if (sound == null) { @@ -774,7 +786,7 @@ namespace Barotrauma { return null; } - bool muffle = !sound.IgnoreMuffling && ShouldMuffleSound(Character.Controlled, position, far, hullGuess); + bool muffle = !ignoreMuffling && ShouldMuffleSound(Character.Controlled, position, far, hullGuess); return sound.Play(volume ?? sound.BaseGain, far, freqMult ?? 1.0f, position, muffle: muffle); } @@ -959,7 +971,7 @@ namespace Barotrauma return "wreck"; } - if (Level.IsLoadedOutpost && Character.Controlled.Submarine == Level.Loaded.StartOutpost) + if (Level.IsLoadedOutpost) { // Only return music type for location types which have music tracks defined var locationType = Level.Loaded.StartLocation?.Type?.Identifier?.ToLowerInvariant(); @@ -1001,7 +1013,7 @@ namespace Barotrauma foreach (Character character in Character.CharacterList) { if (character.IsDead || !character.Enabled) continue; - if (!(character.AIController is EnemyAIController enemyAI) || (!enemyAI.AttackHumans && !enemyAI.AttackRooms)) { continue; } + if (!(character.AIController is EnemyAIController enemyAI) || !enemyAI.Enabled || (!enemyAI.AttackHumans && !enemyAI.AttackRooms)) { continue; } if (targetSubmarine != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs index 207c24449..ae7ed3b35 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs @@ -11,6 +11,9 @@ namespace Barotrauma { public float RotationState; public float OffsetState; + public Vector2 RandomOffsetMultiplier = new Vector2(Rand.Range(-1.0f, 1.0f), Rand.Range(-1.0f, 1.0f)); + public float RandomRotationFactor = Rand.Range(0.0f, 1.0f); + public float RandomScaleFactor = Rand.Range(0.0f, 1.0f); public bool IsActive = true; } @@ -29,6 +32,9 @@ namespace Barotrauma [Serialize("0,0", true), Editable] public Vector2 Offset { get; private set; } + [Serialize("0,0", true), Editable] + public Vector2 RandomOffset { get; private set; } + [Serialize(AnimationType.None, false), Editable] public AnimationType OffsetAnim { get; private set; } @@ -66,6 +72,20 @@ namespace Barotrauma } } + private Vector2 randomRotationRadians; + [Serialize("0,0", true), Editable] + public Vector2 RandomRotation + { + get + { + return new Vector2(MathHelper.ToDegrees(randomRotationRadians.X), MathHelper.ToDegrees(randomRotationRadians.Y)); + } + private set + { + randomRotationRadians = new Vector2(MathHelper.ToRadians(value.X), MathHelper.ToRadians(value.Y)); + } + } + private float scale; [Serialize(1.0f, true), Editable] public float Scale @@ -74,6 +94,13 @@ namespace Barotrauma private set { scale = MathHelper.Clamp(value, 0.0f, 10.0f); } } + [Serialize("0,0", true), Editable] + public Vector2 RandomScale + { + get; + private set; + } + [Serialize(AnimationType.None, false), Editable] public AnimationType RotationAnim { get; private set; } @@ -99,9 +126,10 @@ namespace Barotrauma { Sprite = new Sprite(element, path, file, lazyLoad: lazyLoad); SerializableProperties = SerializableProperty.DeserializeProperties(this, element); - // TODO: what's the purpose of this? + // load property conditionals foreach (XElement subElement in element.Elements()) { + //choose which list the new conditional should be placed to List conditionalList = null; switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -125,7 +153,7 @@ namespace Barotrauma } } - public Vector2 GetOffset(ref float offsetState, float rotation = 0.0f) + public Vector2 GetOffset(ref float offsetState, Vector2 randomOffsetMultiplier, float rotation = 0.0f) { Vector2 offset = Offset; if (OffsetAnimSpeed > 0.0f) @@ -146,6 +174,9 @@ namespace Barotrauma break; } } + offset += new Vector2( + RandomOffset.X * randomOffsetMultiplier.X, + RandomOffset.Y * randomOffsetMultiplier.Y); if (Math.Abs(rotation) > 0.01f) { Matrix transform = Matrix.CreateRotationZ(rotation); @@ -154,24 +185,40 @@ namespace Barotrauma return offset; } - public float GetRotation(ref float rotationState) + public float GetRotation(ref float rotationState, float randomRotationFactor) { RotationSpeed = -Math.Abs(RotationSpeed); switch (RotationAnim) { case AnimationType.Sine: rotationState %= MathHelper.TwoPi / absRotationSpeedRadians; - return rotationRadians * (float)Math.Sin(rotationState * rotationSpeedRadians); + return + rotationRadians * (float)Math.Sin(rotationState * rotationSpeedRadians) + + MathHelper.Lerp(randomRotationRadians.X, randomRotationRadians.Y, randomRotationFactor); case AnimationType.Noise: rotationState %= 1.0f / absRotationSpeedRadians; - return rotationRadians * (PerlinNoise.GetPerlin(rotationState * absRotationSpeedRadians, rotationState * absRotationSpeedRadians) - 0.5f); + return + rotationRadians * (PerlinNoise.GetPerlin(rotationState * absRotationSpeedRadians, rotationState * absRotationSpeedRadians) - 0.5f) + + MathHelper.Lerp(randomRotationRadians.X, randomRotationRadians.Y, randomRotationFactor); default: - return rotationState * rotationSpeedRadians; + return + rotationRadians + + rotationState * rotationSpeedRadians + + MathHelper.Lerp(randomRotationRadians.X, randomRotationRadians.Y, randomRotationFactor); } } - public static void UpdateSpriteStates(Dictionary> spriteGroups, Dictionary animStates, - int entityID, float deltaTime, Func checkConditional) + public float GetScale(float randomScaleModifier) + { + if (RandomScale == Vector2.Zero) + { + return scale; + } + return MathHelper.Lerp(RandomScale.X, RandomScale.Y, randomScaleModifier); + } + + public static void UpdateSpriteStates(Dictionary> spriteGroups, Dictionary animStates, + int entityID, float deltaTime, Func checkConditional) { foreach (int spriteGroup in spriteGroups.Keys) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/PositionalDeformation.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/PositionalDeformation.cs index 6b38a7826..6901234d1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/PositionalDeformation.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/PositionalDeformation.cs @@ -23,13 +23,13 @@ namespace Barotrauma.SpriteDeformations /// /// How fast the sprite reacts to being stretched /// - [Serialize(1.0f, true, description: "How fast the sprite reacts to being stretched"), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] + [Serialize(10.0f, true, description: "How fast the sprite reacts to being stretched"), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] public float ReactionSpeed { get; set; } /// /// How fast the sprite returns back to normal after stretching ends /// - [Serialize(0.1f, true, description: "How fast the sprite returns back to normal after stretching ends"), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] + [Serialize(0.05f, true, description: "How fast the sprite returns back to normal after stretching ends"), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] public float RecoverSpeed { get; set; } public PositionalDeformationParams(XElement element) : base(element) @@ -56,13 +56,13 @@ namespace Barotrauma.SpriteDeformations public override void Update(float deltaTime) { - if (positionalDeformationParams.RecoverSpeed <= 0.0f) return; + if (positionalDeformationParams.RecoverSpeed <= 0.0f) { return; } for (int x = 0; x < Resolution.X; x++) { for (int y = 0; y < Resolution.Y; y++) { - if (Deformation[x,y].LengthSquared() < 0.0001f) + if (Deformation[x,y].LengthSquared() < 0.000001f) { Deformation[x, y] = Vector2.Zero; continue; @@ -78,9 +78,9 @@ namespace Barotrauma.SpriteDeformations { Vector2 pos = Vector2.Transform(worldPosition, transformMatrix); Point deformIndex = new Point((int)(pos.X * (Resolution.X - 1)), (int)(pos.Y * (Resolution.Y - 1))); - - if (deformIndex.X < 0 || deformIndex.Y < 0) return; - if (deformIndex.X >= Resolution.X || deformIndex.Y >= Resolution.Y) return; + + if (deformIndex.X < 0 || deformIndex.Y < 0) { return; } + if (deformIndex.X >= Resolution.X || deformIndex.Y >= Resolution.Y) { return; } amount = amount.ClampLength(positionalDeformationParams.MaxDeformation); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformableSprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformableSprite.cs index bde2b3cb8..45bcb951b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformableSprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformableSprite.cs @@ -296,7 +296,7 @@ namespace Barotrauma Matrix matrix = GetTransform(pos, origin, rotate, scale); effect.Parameters["xTransform"].SetValue(matrix * cam.ShaderTransform - * Matrix.CreateOrthographic(GameMain.GraphicsWidth, GameMain.GraphicsHeight, -1, 1) * 0.5f); + * Matrix.CreateOrthographic(cam.Resolution.X, cam.Resolution.Y, -1, 1) * 0.5f); effect.Parameters["tintColor"].SetValue(color.ToVector4()); effect.Parameters["deformArray"].SetValue(deformAmount); effect.Parameters["deformArrayWidth"].SetValue(deformArrayWidth); diff --git a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs index e8f122f0c..7cd361e71 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs @@ -75,7 +75,7 @@ namespace Barotrauma particleRotation = -item.body.Rotation; if (item.body.Dir < 0.0f) { particleRotation += MathHelper.Pi; } } - else if (entity is Character c && targetLimbs?.FirstOrDefault(l => l != LimbType.None) is LimbType l) + else if (entity is Character c && !c.Removed && targetLimbs?.FirstOrDefault(l => l != LimbType.None) is LimbType l) { targetLimb = c.AnimController.GetLimb(l); } @@ -95,6 +95,8 @@ namespace Barotrauma } } + private bool ignoreMuffling; + private void PlaySound(Entity entity, Hull hull, Vector2 worldPosition) { if (sounds.Count == 0) return; @@ -111,7 +113,8 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("StatusEffect.ApplyProjSpecific:SoundNull1" + Environment.StackTrace.CleanupStackTrace(), GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); return; } - soundChannel = SoundPlayer.PlaySound(sound.Sound, worldPosition, sound.Volume, sound.Range, hullGuess: hull); + soundChannel = SoundPlayer.PlaySound(sound.Sound, worldPosition, sound.Volume, sound.Range, hullGuess: hull, ignoreMuffling: sound.IgnoreMuffling); + ignoreMuffling = sound.IgnoreMuffling; if (soundChannel != null) { soundChannel.Looping = loopSound; } } } @@ -141,7 +144,8 @@ namespace Barotrauma { Submarine.ReloadRoundSound(selectedSound); } - soundChannel = SoundPlayer.PlaySound(selectedSound.Sound, worldPosition, selectedSound.Volume, selectedSound.Range, hullGuess: hull); + soundChannel = SoundPlayer.PlaySound(selectedSound.Sound, worldPosition, selectedSound.Volume, selectedSound.Range, hullGuess: hull, ignoreMuffling: selectedSound.IgnoreMuffling); + ignoreMuffling = selectedSound.IgnoreMuffling; if (soundChannel != null) { soundChannel.Looping = loopSound; } } } @@ -176,7 +180,7 @@ namespace Barotrauma else { statusEffect.soundChannel.Position = new Vector3(statusEffect.soundEmitter.WorldPosition, 0.0f); - if (doMuffleCheck) + if (doMuffleCheck && !statusEffect.ignoreMuffling) { statusEffect.soundChannel.Muffled = SoundPlayer.ShouldMuffleSound( Character.Controlled, statusEffect.soundEmitter.WorldPosition, statusEffect.soundChannel.Far, Character.Controlled?.CurrentHull); diff --git a/Barotrauma/BarotraumaClient/ClientSource/SubEditorCommands.cs b/Barotrauma/BarotraumaClient/ClientSource/SubEditorCommands.cs index df49996e5..c4c40edb9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/SubEditorCommands.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/SubEditorCommands.cs @@ -123,8 +123,9 @@ namespace Barotrauma /// Ignore item inventories when set to false, workaround for pasting public AddOrDeleteCommand(List receivers, bool wasDeleted, bool handleInventoryBehavior = true) { + Debug.Assert(receivers.Count > 0, "Command has 0 receivers"); WasDeleted = wasDeleted; - Receivers = receivers; + Receivers = new List(receivers); try { @@ -132,7 +133,7 @@ namespace Barotrauma { if (receiver is Item it && it.ParentInventory != null) { - PreviousInventories.Add(new InventorySlotItem(Array.IndexOf(it.ParentInventory.Items, it), it), it.ParentInventory); + PreviousInventories.Add(new InventorySlotItem(it.ParentInventory.FindIndex(it), it), it.ParentInventory); } } @@ -146,8 +147,7 @@ namespace Barotrauma foreach (ItemContainer component in it.GetComponents()) { if (component.Inventory == null) { continue; } - - itemsToDelete.AddRange(component.Inventory.Items.Where(item => item != null && !item.Removed)); + itemsToDelete.AddRange(component.Inventory.AllItems.Where(item => !item.Removed)); } } } @@ -309,12 +309,12 @@ namespace Barotrauma { return Receivers.Count > 1 ? TextManager.GetWithVariable("Undo.RemovedItemsMultiple", "[count]", Receivers.Count.ToString()) - : TextManager.GetWithVariable("Undo.RemovedItem", "[item]", Receivers.FirstOrDefault()?.Name); + : TextManager.GetWithVariable("Undo.RemovedItem", "[item]", Receivers.FirstOrDefault()?.Name ?? "null"); } return Receivers.Count > 1 ? TextManager.GetWithVariable("Undo.AddedItemsMultiple", "[count]", Receivers.Count.ToString()) - : TextManager.GetWithVariable("Undo.AddedItem", "[item]", Receivers.FirstOrDefault()?.Name); + : TextManager.GetWithVariable("Undo.AddedItem", "[item]", Receivers.FirstOrDefault()?.Name ?? "null"); } } @@ -332,7 +332,7 @@ namespace Barotrauma public InventoryPlaceCommand(Inventory inventory, List items, bool dropped) { Inventory = inventory; - Receivers = items.Select(item => new InventorySlotItem(Array.IndexOf(inventory.Items, item), item)).ToList(); + Receivers = items.Select(item => new InventorySlotItem(inventory.FindIndex(item), item)).ToList(); wasDropped = dropped; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs index e6e82b012..c209095aa 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs @@ -99,6 +99,94 @@ namespace Barotrauma if (hue < 240) return q1 + (q2 - q1) * (240 - hue) / 60; return q1; } + + /// + /// Convert a HSV value into a RGB value. + /// + /// Value between 0 and 360 + /// Value between 0 and 1 + /// Value between 0 and 1 + /// Reference + /// + public static Color HSVToRGB(float hue, float saturation, float value) + { + float c = value * saturation; + + float h = Math.Clamp(hue, 0, 360) / 60f; + + float x = c * (1 - Math.Abs(h % 2 - 1)); + + float r = 0, + g = 0, + b = 0; + + if (0 <= h && h <= 1) { r = c; g = x; b = 0; } + else if (1 < h && h <= 2) { r = x; g = c; b = 0; } + else if (2 < h && h <= 3) { r = 0; g = c; b = x; } + else if (3 < h && h <= 4) { r = 0; g = x; b = c; } + else if (4 < h && h <= 5) { r = x; g = 0; b = c; } + else if (5 < h && h <= 6) { r = c; g = 0; b = x; } + + float m = value - c; + + return new Color(r + m, g + m, b + m); + } + + /// + /// Convert a RGB value into a HSV value. + /// + /// + /// Reference + /// + /// Vector3 where X is the hue (0-360 or NaN) + /// Y is the saturation (0-1) + /// Z is the value (0-1) + /// + public static Vector3 RGBToHSV(Color color) + { + float r = color.R / 255f, + g = color.G / 255f, + b = color.B / 255f; + + float h, s; + + float min = Math.Min(r, Math.Min(g, b)); + float max = Math.Max(r, Math.Max(g, b)); + + float v = max; + + float delta = max - min; + + if (max != 0) + { + s = delta / max; + } + else + { + s = 0; + h = -1; + return new Vector3(h, s, v); + } + + if (MathUtils.NearlyEqual(r, max)) + { + h = (g - b) / delta; + } + else if (MathUtils.NearlyEqual(g, max)) + { + h = 2 + (b - r) / delta; + } + else + { + h = 4 + (r - g) / delta; + } + + h *= 60; + if (h < 0) { h += 360; } + + return new Vector3(h, s, v); + } + public static Color Add(this Color sourceColor, Color color) { @@ -235,6 +323,7 @@ namespace Barotrauma { linePos = splitSize = 0.0f; splitWord[k] = splitWord[k].Remove(splitWord[k].Length - 1) + "\n"; + if (splitWord[k].Length <= 1) { break; } j--; splitWord.Add(string.Empty); k++; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs new file mode 100644 index 000000000..eb188b43c --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs @@ -0,0 +1,160 @@ +using Barotrauma.IO; +using FarseerPhysics; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Barotrauma +{ + static class WikiImage + { + public static Rectangle CalculateBoundingBox(Character character) + { + Rectangle boundingBox = new Rectangle(character.WorldPosition.ToPoint(), Point.Zero); + + void addPointsToBBox(float extentX, float extentY, Vector2 worldPos, Vector2 origin, float rotation) + { + float sinRotation = (float)Math.Sin((double)rotation); + float cosRotation = (float)Math.Cos((double)rotation); + + origin = new Vector2( + origin.X * cosRotation + origin.Y * sinRotation, + origin.X * sinRotation - origin.Y * cosRotation); + var limbPos = worldPos.ToPoint(); + boundingBox.AddPoint(limbPos); + Vector2 xExtend = new Vector2((extentX * cosRotation), (extentX * sinRotation)); + Vector2 yExtend = new Vector2((extentY * sinRotation), (-extentY * cosRotation)); + boundingBox.AddPoint(limbPos + (xExtend + yExtend - origin).ToPoint()); + boundingBox.AddPoint(limbPos + (xExtend - yExtend - origin).ToPoint()); + boundingBox.AddPoint(limbPos + (-xExtend - yExtend - origin).ToPoint()); + boundingBox.AddPoint(limbPos + (-xExtend + yExtend - origin).ToPoint()); + } + + foreach (Limb limb in character.AnimController.Limbs) + { + if (limb.ActiveSprite == null) { continue; } + float extentX = (float)limb.ActiveSprite.size.X * limb.Scale * limb.TextureScale * 0.5f; + //extentX = ConvertUnits.ToDisplayUnits(extentX); + float extentY = (float)limb.ActiveSprite.size.Y * limb.Scale * limb.TextureScale * 0.5f; + //extentY = ConvertUnits.ToDisplayUnits(extentY); + + Vector2 origin = (limb.ActiveSprite.Origin - (limb.ActiveSprite.SourceRect.Size.ToVector2() * 0.5f)) * limb.Scale * limb.TextureScale; + addPointsToBBox(extentX, extentY, limb.WorldPosition, origin, limb.body.Rotation); + } + + + if (character.Inventory != null) + { + foreach (var item in character.Inventory.AllItems) + { + if (item?.Sprite != null && item?.body != null) + { + float extentX = (float)item.Sprite.size.X * item.Scale * 0.5f; + //extentX = ConvertUnits.ToDisplayUnits(extentX); + float extentY = (float)item.Sprite.size.Y * item.Scale * 0.5f; + //extentY = ConvertUnits.ToDisplayUnits(extentY); + + Vector2 origin = (item.Sprite.Origin - (item.Sprite.SourceRect.Size.ToVector2() * 0.5f)) * item.Scale; + addPointsToBBox(extentX, extentY, item.WorldPosition, origin, item.body.Rotation); + } + } + } + + boundingBox.X -= 25; boundingBox.Y -= 25; + boundingBox.Width += 50; boundingBox.Height += 50; + + return boundingBox; + } + + public static void Create(Character character) + { + Rectangle boundingBox = CalculateBoundingBox(character); + + int texWidth = Math.Clamp((int)(boundingBox.Width * 2.5f), 512, 4096); + float zoom = (float)texWidth / (float)boundingBox.Width; + int texHeight = (int)(zoom * boundingBox.Height); + + Camera cam = new Camera(); + cam.SetResolution(new Point(texWidth, texHeight)); + cam.MaxZoom = zoom; + cam.MinZoom = zoom * 0.5f; + cam.Zoom = zoom; + cam.Position = boundingBox.Center.ToVector2(); + cam.UpdateTransform(false); + + using (RenderTarget2D rt = new RenderTarget2D( + GameMain.Instance.GraphicsDevice, + texWidth, texHeight, false, SurfaceFormat.Color, DepthFormat.None)) + { + using (SpriteBatch spriteBatch = new SpriteBatch(GameMain.Instance.GraphicsDevice)) + { + Viewport prevViewport = GameMain.Instance.GraphicsDevice.Viewport; + GameMain.Instance.GraphicsDevice.Viewport = new Viewport(0, 0, texWidth, texHeight); + GameMain.Instance.GraphicsDevice.SetRenderTarget(rt); + GameMain.Instance.GraphicsDevice.Clear(Color.Transparent); + spriteBatch.Begin(SpriteSortMode.BackToFront, transformMatrix: cam.Transform); + character.Draw(spriteBatch, cam); + if (character.Inventory != null) + { + foreach (var item in character.Inventory.AllItems) + { + if (item != null) + { + item.Draw(spriteBatch, false, false); + item.Draw(spriteBatch, false, true); + } + } + } + spriteBatch.End(); + GameMain.Instance.GraphicsDevice.SetRenderTarget(null); + GameMain.Instance.GraphicsDevice.Viewport = prevViewport; + using (FileStream fs = File.Open("wikiimage.png", System.IO.FileMode.Create)) + { + rt.SaveAsPng(fs, boundingBox.Width, boundingBox.Height); + } + } + } + } + + public static void Create(Submarine sub) + { + int width = 4096; int height = 4096; + + Rectangle subDimensions = sub.CalculateDimensions(false); + Vector2 viewPos = subDimensions.Center.ToVector2(); + float scale = Math.Min(width / (float)subDimensions.Width, height / (float)subDimensions.Height); + + var viewMatrix = Matrix.CreateTranslation(new Vector3(width / 2.0f, height / 2.0f, 0)); + var transform = Matrix.CreateTranslation( + new Vector3(-viewPos.X, viewPos.Y, 0)) * + Matrix.CreateScale(new Vector3(scale, scale, 1)) * + viewMatrix; + + using (RenderTarget2D rt = new RenderTarget2D( + GameMain.Instance.GraphicsDevice, + width, height, false, SurfaceFormat.Color, DepthFormat.None)) + using (SpriteBatch spriteBatch = new SpriteBatch(GameMain.Instance.GraphicsDevice)) + { + Viewport prevViewport = GameMain.Instance.GraphicsDevice.Viewport; + GameMain.Instance.GraphicsDevice.Viewport = new Viewport(0, 0, width, height); + GameMain.Instance.GraphicsDevice.SetRenderTarget(rt); + GameMain.Instance.GraphicsDevice.Clear(Color.Transparent); + + spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, null, null, null, null, transform); + Submarine.Draw(spriteBatch); + Submarine.DrawFront(spriteBatch); + Submarine.DrawDamageable(spriteBatch, null); + spriteBatch.End(); + + GameMain.Instance.GraphicsDevice.SetRenderTarget(null); + GameMain.Instance.GraphicsDevice.Viewport = prevViewport; + using (FileStream fs = File.Open("wikiimage.png", System.IO.FileMode.Create)) + { + rt.SaveAsPng(fs, width, height); + } + } + } + } +} diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index e45443392..bd6036610 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.11.0.10 + 0.12.0.2 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index c798f8c5b..5af83bd41 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.11.0.10 + 0.12.0.2 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 664371851..635305e76 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.11.0.10 + 0.12.0.2 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/soft_oal_x64.dll b/Barotrauma/BarotraumaClient/soft_oal_x64.dll index 7ca46d8c3..707098d6e 100644 Binary files a/Barotrauma/BarotraumaClient/soft_oal_x64.dll and b/Barotrauma/BarotraumaClient/soft_oal_x64.dll differ diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index ef56e6f87..f82daf131 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.11.0.9 + 0.12.0.2 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 8f712c24a..3432ec711 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.11.0.9 + 0.12.0.2 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs index a9d2205c5..0aa71f569 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs @@ -1,9 +1,27 @@ using Barotrauma.Networking; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; namespace Barotrauma { partial class CharacterInfo { + private readonly Dictionary prevSentSkill = new Dictionary(); + + partial void OnSkillChanged(string skillIdentifier, float prevLevel, float newLevel, Vector2 textPopupPos) + { + if (!prevSentSkill.ContainsKey(skillIdentifier)) + { + prevSentSkill[skillIdentifier] = prevLevel; + } + if (Math.Abs(prevSentSkill[skillIdentifier] - newLevel) > 0.01f) + { + GameMain.NetworkMember.CreateEntityEvent(Character, new object[] { NetEntityEvent.Type.UpdateSkills }); + prevSentSkill[skillIdentifier] = newLevel; + } + } + public void ServerWrite(IWriteMessage msg) { msg.Write(ID); diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index 8d7fdfd36..9807fc34d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -1345,7 +1345,15 @@ namespace Barotrauma commands.Add(new Command("startgame|startround|start", "start/startgame/startround: Start a new round.", (string[] args) => { if (Screen.Selected == GameMain.GameScreen) { return; } - if (!GameMain.Server.StartGame()) NewMessage("Failed to start a new round", Color.Yellow); + if (GameMain.GameSession?.GameMode is MultiPlayerCampaign mpCampaign && + GameMain.NetLobbyScreen.SelectedMode == GameModePreset.MultiPlayerCampaign) + { + MultiPlayerCampaign.LoadCampaign(GameMain.GameSession.SavePath); + } + else + { + if (!GameMain.Server.StartGame()) { NewMessage("Failed to start a new round", Color.Yellow); } + } })); commands.Add(new Command("endgame|endround|end", "end/endgame/endround: End the current round.", (string[] args) => diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/BeaconMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/BeaconMission.cs index 1c590608a..0f0a29d29 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/BeaconMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/BeaconMission.cs @@ -1,7 +1,4 @@ using Barotrauma.Networking; -using System; -using System.Collections.Generic; -using System.Text; namespace Barotrauma { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs index 170b5b538..123f2e422 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs @@ -21,31 +21,6 @@ namespace Barotrauma } } - public override void AssignTeamIDs(List clients) - { - List randList = new List(clients); - for (int i = 0; i < randList.Count; i++) - { - Client a = randList[i]; - int oi = Rand.Range(0, randList.Count - 1); - Client b = randList[oi]; - randList[i] = b; - randList[oi] = a; - } - int halfPlayers = randList.Count / 2; - for (int i = 0; i < randList.Count; i++) - { - if (i < halfPlayers) - { - randList[i].TeamID = Character.TeamType.Team1; - } - else - { - randList[i].TeamID = Character.TeamType.Team2; - } - } - } - public override void Update(float deltaTime) { if (!initialized) @@ -54,11 +29,11 @@ namespace Barotrauma crews[1].Clear(); foreach (Character character in Character.CharacterList) { - if (character.TeamID == Character.TeamType.Team1) + if (character.TeamID == CharacterTeamType.Team1) { crews[0].Add(character); } - else if (character.TeamID == Character.TeamType.Team2) + else if (character.TeamID == CharacterTeamType.Team2) { crews[1].Add(character); } @@ -88,7 +63,7 @@ namespace Barotrauma //make sure nobody in the other team can be revived because that would be pretty weird crews[1 - i].ForEach(c => { if (!c.IsDead) c.Kill(CauseOfDeathType.Unknown, null); }); - GameMain.GameSession.WinningTeam = i == 0 ? Character.TeamType.Team1 : Character.TeamType.Team2; + GameMain.GameSession.WinningTeam = i == 0 ? CharacterTeamType.Team1 : CharacterTeamType.Team2; state = 1; break; @@ -99,10 +74,10 @@ namespace Barotrauma { if (teamDead[0] && teamDead[1]) { - GameMain.GameSession.WinningTeam = Character.TeamType.None; + GameMain.GameSession.WinningTeam = CharacterTeamType.None; if (GameMain.Server != null) { GameMain.Server.EndGame(); } } - else if (GameMain.GameSession.WinningTeam != Character.TeamType.None) + else if (GameMain.GameSession.WinningTeam != CharacterTeamType.None) { GameMain.Server.EndGame(); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MineralMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MineralMission.cs index 354df9c7a..b6555cb25 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MineralMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MineralMission.cs @@ -6,6 +6,12 @@ namespace Barotrauma { public override void ServerWriteInitial(IWriteMessage msg, Client c) { + msg.Write((byte)caves.Count); + foreach (var cave in caves) + { + msg.Write((byte)(Level.Loaded == null || !Level.Loaded.Caves.Contains(cave) ? 255 : Level.Loaded.Caves.IndexOf(cave))); + } + foreach (var kvp in SpawnedResources) { msg.Write((byte)kvp.Value.Count); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/NestMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/NestMission.cs index 17900d793..d1b702aa1 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/NestMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/NestMission.cs @@ -4,8 +4,11 @@ namespace Barotrauma { partial class NestMission : Mission { + private Level.Cave selectedCave; + public override void ServerWriteInitial(IWriteMessage msg, Client c) { + msg.Write((byte)(selectedCave == null || Level.Loaded == null || !Level.Loaded.Caves.Contains(selectedCave) ? 255 : Level.Loaded.Caves.IndexOf(selectedCave))); msg.Write(nestPosition.X); msg.Write(nestPosition.Y); msg.Write((ushort)items.Count); diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs index dc610006b..fff520d8f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CargoManager.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; namespace Barotrauma { @@ -6,9 +7,12 @@ namespace Barotrauma { public void SellBackPurchasedItems(List itemsToSell) { + // Check all the prices before starting the transaction + // to make sure the modifiers stay the same for the whole transaction + Dictionary buyValues = GetBuyValuesAtCurrentLocation(itemsToSell.Select(i => i.ItemPrefab)); foreach (PurchasedItem item in itemsToSell) { - var itemValue = GetBuyValueAtCurrentLocation(item); + var itemValue = item.Quantity * buyValues[item.ItemPrefab]; Location.StoreCurrentBalance -= itemValue; campaign.Money += itemValue; PurchasedItems.Remove(item); @@ -17,9 +21,12 @@ namespace Barotrauma public void BuyBackSoldItems(List itemsToBuy) { + // Check all the prices before starting the transaction + // to make sure the modifiers stay the same for the whole transaction + Dictionary sellValues = GetSellValuesAtCurrentLocation(itemsToBuy.Select(i => i.ItemPrefab)); foreach (SoldItem item in itemsToBuy) { - var itemValue = GetSellValueAtCurrentLocation(item.ItemPrefab); + var itemValue = sellValues[item.ItemPrefab]; if (Location.StoreCurrentBalance < itemValue || item.Removed) { continue; } Location.StoreCurrentBalance += itemValue; campaign.Money -= itemValue; @@ -29,10 +36,13 @@ namespace Barotrauma public void SellItems(List itemsToSell) { + // Check all the prices before starting the transaction + // to make sure the modifiers stay the same for the whole transaction + Dictionary sellValues = GetSellValuesAtCurrentLocation(itemsToSell.Select(i => i.ItemPrefab)); var canAddToRemoveQueue = (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) && Entity.Spawner != null; foreach (SoldItem item in itemsToSell) { - var itemValue = GetSellValueAtCurrentLocation(item.ItemPrefab); + var itemValue = sellValues[item.ItemPrefab]; // check if the store can afford the item and if the item hasn't been removed already if (Location.StoreCurrentBalance < itemValue || item.Removed) { continue; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index 0171ac8f1..36e237a51 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -225,7 +225,7 @@ namespace Barotrauma if (c.Inventory == null) { continue; } if (Level.Loaded.Type == LevelData.LevelType.Outpost && c.Submarine != Level.Loaded.StartOutpost) { - Map.CurrentLocation.RegisterTakenItems(c.Inventory.Items.Where(it => it != null && it.SpawnedInOutpost && it.OriginalModuleIndex > 0).Distinct()); + Map.CurrentLocation.RegisterTakenItems(c.Inventory.AllItems.Where(it => it.SpawnedInOutpost && it.OriginalModuleIndex > 0)); } if (c.Info != null && c.IsBot) @@ -773,6 +773,9 @@ namespace Barotrauma element.Add(new XAttribute("campaignid", CampaignID)); XElement modeElement = new XElement("MultiPlayerCampaign", new XAttribute("money", Money), + new XAttribute("purchasedlostshuttles", PurchasedLostShuttles), + new XAttribute("purchasedhullrepairs", PurchasedHullRepairs), + new XAttribute("purchaseditemrepairs", PurchasedItemRepairs), new XAttribute("cheatsenabled", CheatsEnabled)); CampaignMetadata?.Save(modeElement); Map.Save(modeElement); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/DockingPort.cs index 72d130ff1..152fe95e5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/DockingPort.cs @@ -5,7 +5,6 @@ namespace Barotrauma.Items.Components { partial class DockingPort : ItemComponent, IDrawableComponent, IServerSerializable { - private UInt16 originalDockingTargetID; public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) @@ -15,7 +14,7 @@ namespace Barotrauma.Items.Components if (docked) { msg.Write(originalDockingTargetID); - msg.Write(hulls != null && hulls[0] != null && hulls[1] != null && gap != null); + msg.Write(IsLocked); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs index c92401bec..02bcb9320 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/ItemLabel.cs @@ -32,6 +32,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize("0,0,0,0", true, description: "The amount of padding around the text in pixels (left,top,right,bottom).")] + public Vector4 Padding + { + get; + set; + } + public override void Move(Vector2 amount) { //do nothing diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs index 3d6955aef..61f312772 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs @@ -1,10 +1,7 @@ using Barotrauma.Networking; -using FarseerPhysics; -using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Items.Components { @@ -18,7 +15,7 @@ namespace Barotrauma.Items.Components for (int i = 0; i < Connections.Count; i++) { wires[i] = new List(); - for (int j = 0; j < Connection.MaxLinked; j++) + for (int j = 0; j < Connections[i].MaxWires; j++) { ushort wireId = msg.ReadUInt16(); @@ -68,9 +65,9 @@ namespace Barotrauma.Items.Components { item.CreateServerEvent(this); c.Character.Inventory?.CreateNetworkEvent(); - for (int i = 0; i < 2; i++) + foreach (Item heldItem in c.Character.HeldItems) { - var selectedWire = c.Character.SelectedItems[i]?.GetComponent(); + var selectedWire = heldItem?.GetComponent(); if (selectedWire == null) { continue; } selectedWire.CreateNetworkEvent(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CustomInterface.cs index 4d15cc9e2..ae6f5b6ef 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CustomInterface.cs @@ -1,7 +1,5 @@ using Barotrauma.Networking; -using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma.Items.Components { @@ -13,7 +11,7 @@ namespace Barotrauma.Items.Components string[] elementValues = new string[customInterfaceElementList.Count]; for (int i = 0; i < customInterfaceElementList.Count; i++) { - if (!string.IsNullOrEmpty(customInterfaceElementList[i].PropertyName)) + if (customInterfaceElementList[i].HasPropertyName) { elementValues[i] = msg.ReadString(); } @@ -28,9 +26,17 @@ namespace Barotrauma.Items.Components { for (int i = 0; i < customInterfaceElementList.Count; i++) { - if (!string.IsNullOrEmpty(customInterfaceElementList[i].PropertyName)) + if (customInterfaceElementList[i].HasPropertyName) { - TextChanged(customInterfaceElementList[i], elementValues[i]); + if (!customInterfaceElementList[i].IsIntegerInput) + { + TextChanged(customInterfaceElementList[i], elementValues[i]); + } + else + { + int.TryParse(elementValues[i], out int value); + ValueChanged(customInterfaceElementList[i], value); + } } else if (customInterfaceElementList[i].ContinuousSignal) { @@ -60,7 +66,7 @@ namespace Barotrauma.Items.Components //extradata contains an array of buttons clicked by a client (or nothing if nothing was clicked) for (int i = 0; i < customInterfaceElementList.Count; i++) { - if (!string.IsNullOrEmpty(customInterfaceElementList[i].PropertyName)) + if (customInterfaceElementList[i].HasPropertyName) { msg.Write(customInterfaceElementList[i].Signal); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs index 4e9fddeb4..e36e3e67f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs @@ -37,9 +37,16 @@ namespace Barotrauma.Items.Components public void SyncHistory() { //split too long messages to multiple parts + int msgIndex = 0; foreach (string str in messageHistory) { string msgToSend = str; + if (string.IsNullOrEmpty(msgToSend)) + { + item.CreateServerEvent(this, new object[] { msgIndex, msgToSend }); + msgIndex++; + continue; + } if (msgToSend.Length > MaxMessageLength) { List splitMessage = msgToSend.Split(' ').ToList(); @@ -62,20 +69,21 @@ namespace Barotrauma.Items.Components if (!splitMessage.Any()) { break; } tempMsg += " "; } while (tempMsg.Length + splitMessage[0].Length < MaxMessageLength); - item.CreateServerEvent(this, new string[] { msgToSend }); + item.CreateServerEvent(this, new object[] { msgIndex, tempMsg }); msgToSend = msgToSend.Remove(0, tempMsg.Length); } } if (!string.IsNullOrEmpty(msgToSend)) { - item.CreateServerEvent(this, new string[] { msgToSend }); - } - } + item.CreateServerEvent(this, new object[] { msgIndex, msgToSend }); + } + msgIndex++; + } } public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { - if (extraData.Length > 2 && extraData[2] is string str) + if (extraData.Length > 3 && extraData[3] is string str) { msg.Write(str); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs index fe0043be8..2c44787e5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs @@ -10,25 +10,30 @@ namespace Barotrauma { public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) { - List prevItems = new List(Items); + List prevItems = new List(AllItems.Distinct()); - byte itemCount = msg.ReadByte(); - ushort[] newItemIDs = new ushort[itemCount]; - for (int i = 0; i < itemCount; i++) + byte slotCount = msg.ReadByte(); + List[] newItemIDs = new List[slotCount]; + for (int i = 0; i < slotCount; i++) { - newItemIDs[i] = msg.ReadUInt16(); + newItemIDs[i] = new List(); + int itemCount = msg.ReadRangedInteger(0, MaxStackSize); + for (int j = 0; j < itemCount; j++) + { + newItemIDs[i].Add(msg.ReadUInt16()); + } } - - if (c == null || c.Character == null) return; + + if (c == null || c.Character == null) { return; } bool accessible = c.Character.CanAccessInventory(this); - if (this is CharacterInventory && accessible) + if (this is CharacterInventory characterInventory && accessible) { - if (Owner == null || !(Owner is Character)) + if (Owner == null || !(Owner is Character ownerCharacter)) { accessible = false; } - else if (!((CharacterInventory)this).AccessibleWhenAlive && !((Character)Owner).IsDead) + else if (!characterInventory.AccessibleWhenAlive && !ownerCharacter.IsDead) { accessible = false; } @@ -42,28 +47,28 @@ namespace Barotrauma CreateNetworkEvent(); for (int i = 0; i < capacity; i++) { - if (!(Entity.FindEntityByID(newItemIDs[i]) is Item item)) { continue; } - item.PositionUpdateInterval = 0.0f; - if (item.ParentInventory != null && item.ParentInventory != this) + foreach (ushort id in newItemIDs[i]) { - item.ParentInventory.CreateNetworkEvent(); + if (!(Entity.FindEntityByID(id) is Item item)) { continue; } + item.PositionUpdateInterval = 0.0f; + if (item.ParentInventory != null && item.ParentInventory != this) + { + item.ParentInventory.CreateNetworkEvent(); + } } } return; } - - List prevItemInventories = new List(Items.Select(i => i?.ParentInventory)); + + List prevItemInventories = new List() { this }; for (int i = 0; i < capacity; i++) { - Item newItem = newItemIDs[i] == 0 ? null : Entity.FindEntityByID(newItemIDs[i]) as Item; - prevItemInventories.Add(newItem?.ParentInventory); - - if (newItemIDs[i] == 0 || (newItem != Items[i])) + foreach (Item item in slots[i].Items.ToList()) { - if (Items[i] != null) + if (!newItemIDs[i].Contains(item.ID)) { - Item droppedItem = Items[i]; + Item droppedItem = item; Entity prevOwner = Owner; droppedItem.Drop(null); @@ -84,15 +89,20 @@ namespace Barotrauma droppedItem.body.SetTransform(prevOwner.SimPosition, 0.0f); } } - System.Diagnostics.Debug.Assert(Items[i] == null); } + + foreach (ushort id in newItemIDs[i]) + { + Item newItem = id == 0 ? null : Entity.FindEntityByID(id) as Item; + prevItemInventories.Add(newItem?.ParentInventory); + } } for (int i = 0; i < capacity; i++) { - if (newItemIDs[i] > 0) + foreach (ushort id in newItemIDs[i]) { - if (!(Entity.FindEntityByID(newItemIDs[i]) is Item item) || item == Items[i]) { continue; } + if (!(Entity.FindEntityByID(id) is Item item) || slots[i].Contains(item)) { continue; } if (GameMain.Server != null) { @@ -101,9 +111,9 @@ namespace Barotrauma if (!prevItems.Contains(item) && !item.CanClientAccess(c)) { -#if DEBUG || UNSTABLE + #if DEBUG || UNSTABLE DebugConsole.NewMessage($"Client {c.Name} failed to pick up item \"{item}\" (parent inventory: {(item.ParentInventory?.Owner.ToString() ?? "null")}). No access.", Color.Yellow); -#endif + #endif if (item.body != null && !c.PendingPositionUpdates.Contains(item)) { c.PendingPositionUpdates.Enqueue(item); @@ -115,9 +125,9 @@ namespace Barotrauma TryPutItem(item, i, true, true, c.Character, false); for (int j = 0; j < capacity; j++) { - if (Items[j] == item && newItemIDs[j] != item.ID) + if (slots[j].Contains(item) && !newItemIDs[j].Contains(item.ID)) { - Items[j] = null; + slots[j].RemoveItem(item); } } } @@ -129,7 +139,7 @@ namespace Barotrauma if (prevInventory != this) { prevInventory?.CreateNetworkEvent(); } } - foreach (Item item in Items.Distinct()) + foreach (Item item in AllItems.Distinct()) { if (item == null) { continue; } if (!prevItems.Contains(item)) @@ -148,7 +158,7 @@ namespace Barotrauma foreach (Item item in prevItems.Distinct()) { if (item == null) { continue; } - if (!Items.Contains(item)) + if (!AllItems.Contains(item)) { if (Owner == c.Character) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs index 68fadc590..e7744bf45 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs @@ -225,7 +225,7 @@ namespace Barotrauma byte decalIndex = msg.ReadByte(); float decalAlpha = msg.ReadRangedSingle(0.0f, 1.0f, 255); if (decalIndex < 0 || decalIndex >= decals.Count) { return; } - if (c.Character != null && c.Character.AllowInput && c.Character.SelectedItems.Any(it => it?.GetComponent() != null)) + if (c.Character != null && c.Character.AllowInput && c.Character.HeldItems.Any(it => it.GetComponent() != null)) { decals[decalIndex].BaseAlpha = decalAlpha; } @@ -242,7 +242,7 @@ namespace Barotrauma Color color = new Color(msg.ReadUInt32()); //TODO: verify the client is close enough to this hull to paint it, that the sprayer is functional and that the color matches - if (c.Character != null && c.Character.AllowInput && c.Character.SelectedItems.Any(it => it?.GetComponent() != null)) + if (c.Character != null && c.Character.AllowInput && c.Character.HeldItems.Any(it => it.GetComponent() != null)) { BackgroundSections[i].SetColorStrength(colorStrength); BackgroundSections[i].SetColor(color); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs index 39faa43e3..3bf2746d7 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs @@ -156,11 +156,17 @@ namespace Barotrauma.Networking switch (orderTargetType) { case Order.OrderTargetType.Entity: - (orderTargetEntity as MapEntity)?.SetIgnoreByAI(orderMsg.Order.Identifier == "ignorethis"); + if (orderTargetEntity is IIgnorable ignorableEntity) + { + ignorableEntity.OrderedToBeIgnored = orderMsg.Order.Identifier == "ignorethis"; + } break; case Order.OrderTargetType.WallSection: if (!wallSectionIndex.HasValue) { break; } - (orderTargetEntity as Structure)?.GetSection(wallSectionIndex.Value)?.SetIgnoreByAI(orderMsg.Order.Identifier == "ignorethis"); + if (orderTargetEntity is Structure s && s.GetSection(wallSectionIndex.Value) is IIgnorable ignorableWall) + { + ignorableWall.OrderedToBeIgnored = orderMsg.Order.Identifier == "ignorethis"; + } break; } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 37aa70cee..a0ed09782 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -35,10 +35,10 @@ namespace Barotrauma.Networking public bool SubmarineSwitchLoad = false; - private List connectedClients = new List(); + private readonly List connectedClients = new List(); //for keeping track of disconnected clients in case the reconnect shortly after - private List disconnectedClients = new List(); + private readonly List disconnectedClients = new List(); //keeps track of players who've previously been playing on the server //so kick votes persist during the session and the server can let the clients know what name this client used previously @@ -1250,22 +1250,30 @@ namespace Barotrauma.Networking bool range = inc.ReadBoolean(); double durationSeconds = inc.ReadDouble(); + TimeSpan? banDuration = null; + if (durationSeconds > 0) { banDuration = TimeSpan.FromSeconds(durationSeconds); } + var bannedClient = connectedClients.Find(cl => cl != sender && cl.Name.Equals(bannedName, StringComparison.OrdinalIgnoreCase) && cl.Connection != OwnerConnection); if (bannedClient != null) { Log("Client \"" + ClientLogName(sender) + "\" banned \"" + ClientLogName(bannedClient) + "\".", ServerLog.MessageType.ServerMessage); if (durationSeconds > 0) { - BanClient(bannedClient, string.IsNullOrEmpty(banReason) ? $"ServerMessage.BannedBy~[initiator]={sender.Name}" : banReason, range, TimeSpan.FromSeconds(durationSeconds)); - } - else - { - BanClient(bannedClient, string.IsNullOrEmpty(banReason) ? $"ServerMessage.BannedBy~[initiator]={sender.Name}" : banReason, range); + BanClient(bannedClient, string.IsNullOrEmpty(banReason) ? $"ServerMessage.BannedBy~[initiator]={sender.Name}" : banReason, range, banDuration); } } else { - SendDirectChatMessage(TextManager.GetServerMessage($"ServerMessage.PlayerNotFound~[player]={bannedName}"), sender, ChatMessageType.Console); + var bannedPreviousClient = previousPlayers.Find(p => p.Name.Equals(bannedName, StringComparison.OrdinalIgnoreCase)); + if (bannedPreviousClient != null) + { + Log("Client \"" + ClientLogName(sender) + "\" banned \"" + bannedPreviousClient.Name + "\".", ServerLog.MessageType.ServerMessage); + BanPreviousPlayer(bannedPreviousClient, string.IsNullOrEmpty(banReason) ? $"ServerMessage.BannedBy~[initiator]={sender.Name}" : banReason, range, banDuration); + } + else + { + SendDirectChatMessage(TextManager.GetServerMessage($"ServerMessage.PlayerNotFound~[player]={bannedName}"), sender, ChatMessageType.Console); + } } break; case ClientPermissions.Unban: @@ -1649,6 +1657,7 @@ namespace Barotrauma.Networking IWriteMessage tempBuffer = new ReadWriteMessage(); tempBuffer.Write((byte)ServerNetObject.ENTITY_POSITION); + tempBuffer.Write(entity is Item); if (entity is Item) { ((Item)entity).ServerWritePosition(tempBuffer, c); @@ -1745,6 +1754,7 @@ namespace Barotrauma.Networking outmsg.Write(client.NameID); outmsg.Write(client.Name); outmsg.Write(client.Character?.Info?.Job != null && gameStarted ? client.Character.Info.Job.Prefab.Identifier : (client.PreferredJob ?? "")); + outmsg.Write((byte)client.PreferredTeam); outmsg.Write(client.Character == null || !gameStarted ? (ushort)0 : client.Character.ID); if (c.HasPermission(ClientPermissions.ServerLog)) { @@ -2071,14 +2081,14 @@ namespace Barotrauma.Networking //always allow the server owner to spectate even if it's disallowed in server settings playingClients.RemoveAll(c => c.Connection == OwnerConnection && c.SpectateOnly); - if (GameMain.GameSession.GameMode.Mission != null) + if (GameMain.GameSession.GameMode is PvPMode pvpMode) { - GameMain.GameSession.GameMode.Mission.AssignTeamIDs(playingClients); - teamCount = GameMain.GameSession.GameMode.Mission.TeamCount; + pvpMode.AssignTeamIDs(playingClients); + teamCount = 2; } else { - connectedClients.ForEach(c => c.TeamID = Character.TeamType.Team1); + connectedClients.ForEach(c => c.TeamID = CharacterTeamType.Team1); } if (campaign != null) @@ -2150,7 +2160,7 @@ namespace Barotrauma.Networking //assign jobs and spawnpoints separately for each team for (int n = 0; n < teamCount; n++) { - var teamID = n == 0 ? Character.TeamType.Team1 : Character.TeamType.Team2; + var teamID = n == 0 ? CharacterTeamType.Team1 : CharacterTeamType.Team2; Submarine.MainSubs[n].TeamID = teamID; foreach (Item item in Item.ItemList) @@ -2177,7 +2187,7 @@ namespace Barotrauma.Networking //always allow the server owner to spectate even if it's disallowed in server settings teamClients.RemoveAll(c => c.Connection == OwnerConnection && c.SpectateOnly); - if (!teamClients.Any() && n > 0) { continue; } + //if (!teamClients.Any() && n > 0) { continue; } AssignJobs(teamClients); @@ -2196,6 +2206,10 @@ namespace Barotrauma.Networking { client.CharacterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, client.Name); } + else + { + client.CharacterInfo.ResetCurrentOrder(); + } characterInfos.Add(client.CharacterInfo); if (client.CharacterInfo.Job == null || client.CharacterInfo.Job.Prefab != client.AssignedJob.First) { @@ -2527,6 +2541,7 @@ namespace Barotrauma.Networking foreach (Client client in connectedClients) { serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); + client.Character?.ResetCurrentOrder(); client.Character = null; client.HasSpawned = false; client.InGame = false; @@ -2563,13 +2578,15 @@ namespace Barotrauma.Networking UInt16 nameId = inc.ReadUInt16(); string newName = inc.ReadString(); string newJob = inc.ReadString(); + CharacterTeamType newTeam = (CharacterTeamType)inc.ReadByte(); if (c == null || string.IsNullOrEmpty(newName) || !NetIdUtils.IdMoreRecent(nameId, c.NameID)) { return false; } c.NameID = nameId; newName = Client.SanitizeName(newName); - if (newName == c.Name && newJob == c.PreferredJob) { return false; } + if (newName == c.Name && newJob == c.PreferredJob && newTeam == c.PreferredTeam) { return false; } c.PreferredJob = newJob; + c.PreferredTeam = newTeam; //update client list even if the name cannot be changed to the one sent by the client, //so the client will be informed what their actual name is @@ -2683,7 +2700,6 @@ namespace Barotrauma.Networking lidgrenConn.IPEndPoint.Address.MapToIPv4NoThrow().ToString() : lidgrenConn.IPEndPoint.Address.ToString(); if (range) { ip = BanList.ToRange(ip); } - serverSettings.BanList.BanPlayer(client.Name, ip, reason, duration); } if (client.SteamID > 0) @@ -2692,6 +2708,32 @@ namespace Barotrauma.Networking } } + public void BanPreviousPlayer(PreviousPlayer previousPlayer, string reason, bool range = false, TimeSpan? duration = null) + { + if (previousPlayer == null) { return; } + + //reset karma to a neutral value, so if/when the ban is revoked the client wont get immediately punished by low karma again + previousPlayer.Karma = Math.Max(previousPlayer.Karma, 50.0f); + + if (!string.IsNullOrEmpty(previousPlayer.EndPoint) && (previousPlayer.SteamID == 0 || range)) + { + string ip = previousPlayer.EndPoint; + if (range) { ip = BanList.ToRange(ip); } + serverSettings.BanList.BanPlayer(previousPlayer.Name, ip, reason, duration); + } + if (previousPlayer.SteamID > 0) + { + serverSettings.BanList.BanPlayer(previousPlayer.Name, previousPlayer.SteamID, reason, duration); + } + + string msg = $"ServerMessage.BannedFromServer~[client]={previousPlayer.Name}"; + if (!string.IsNullOrWhiteSpace(reason)) + { + msg += $"/ /ServerMessage.Reason/: /{reason}"; + } + SendChatMessage(msg, ChatMessageType.Server, changeType: PlayerConnectionChangeType.Banned); + } + public override void UnbanPlayer(string playerName, string playerEndPoint) { if (!string.IsNullOrEmpty(playerEndPoint)) @@ -2734,8 +2776,8 @@ namespace Barotrauma.Networking client.HasSpawned = false; client.InGame = false; - if (string.IsNullOrWhiteSpace(msg)) msg = $"ServerMessage.ClientLeftServer~[client]={client.Name}"; - if (string.IsNullOrWhiteSpace(targetmsg)) targetmsg = "ServerMessage.YouLeftServer"; + if (string.IsNullOrWhiteSpace(msg)) { msg = $"ServerMessage.ClientLeftServer~[client]={client.Name}"; } + if (string.IsNullOrWhiteSpace(targetmsg)) { targetmsg = "ServerMessage.YouLeftServer"; } if (!string.IsNullOrWhiteSpace(reason)) { msg += $"/ /ServerMessage.Reason/: /{reason}"; @@ -2945,14 +2987,14 @@ namespace Barotrauma.Networking { case ChatMessageType.Radio: case ChatMessageType.Order: - if (senderCharacter == null) return; + if (senderCharacter == null) { return; } //return if senderCharacter doesn't have a working radio - var radio = senderCharacter.Inventory?.Items.FirstOrDefault(i => i != null && i.GetComponent() != null); - if (radio == null || !senderCharacter.HasEquippedItem(radio)) return; + var radio = senderCharacter.Inventory?.AllItems.FirstOrDefault(i => i.GetComponent() != null); + if (radio == null || !senderCharacter.HasEquippedItem(radio)) { return; } senderRadio = radio.GetComponent(); - if (!senderRadio.CanTransmit()) return; + if (!senderRadio.CanTransmit()) { return; } break; case ChatMessageType.Dead: //character still alive and capable of speaking -> dead chat not allowed @@ -3167,8 +3209,8 @@ namespace Barotrauma.Networking if (voteType != VoteType.PurchaseSub) { - GameMain.GameSession.SwitchSubmarine(targetSubmarine, deliveryFee); - GameMain.GameSession.Campaign.UpgradeManager.RefundResetAndReload(targetSubmarine, true); + SubmarineInfo newSub = GameMain.GameSession.SwitchSubmarine(targetSubmarine, deliveryFee); + GameMain.GameSession.Campaign.UpgradeManager.RefundResetAndReload(newSub, true); } serverSettings.Voting.StopSubmarineVote(true); @@ -3396,7 +3438,7 @@ namespace Barotrauma.Networking assignedClientCount.Add(jp, 0); } - Character.TeamType teamID = Character.TeamType.None; + CharacterTeamType teamID = CharacterTeamType.None; if (unassigned.Count > 0) { teamID = unassigned[0].TeamID; } //if we're playing a multiplayer campaign, check which clients already have a character and a job @@ -3558,7 +3600,7 @@ namespace Barotrauma.Networking } } - public void AssignBotJobs(List bots, Character.TeamType teamID) + public void AssignBotJobs(List bots, CharacterTeamType teamID) { Dictionary assignedPlayerCount = new Dictionary(); foreach (JobPrefab jp in JobPrefab.Prefabs) @@ -3675,7 +3717,7 @@ namespace Barotrauma.Networking { retVal += "color:#ff9900;"; } - retVal += "metadata:" + (client.SteamID!=0 ? client.SteamID.ToString() : client.ID.ToString()) + "‖" + (name ?? client.Name) + "‖end‖"; + retVal += "metadata:" + (client.SteamID != 0 ? client.SteamID.ToString() : client.ID.ToString()) + "‖" + (name ?? client.Name) + "‖end‖"; return retVal; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs index 54969054e..e4976ad1a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs @@ -256,8 +256,28 @@ namespace Barotrauma return; } } - - var foundItem = Inventory.FindItemRecursive(item, it => it.Prefab.Identifier == "idcard" || it.GetComponent() != null || it.GetComponent() != null); + + Item foundItem = null; + if (isValid(item)) + { + foundItem = item; + } + else + { + foreach (Item containedItem in item.ContainedItems) + { + if (isValid(containedItem)) + { + foundItem = containedItem; + break; + } + } + } + + static bool isValid(Item item) + { + return item.Prefab.Identifier == "idcard" || item.GetComponent() != null || item.GetComponent() != null; + } if (foundItem == null) { return; } @@ -382,7 +402,7 @@ namespace Barotrauma //smaller karma penalty for attacking someone who's aiming with a weapon if (damage > 0.0f && target.IsKeyDown(InputType.Aim) && - target.SelectedItems.Any(it => it != null && (it.GetComponent() != null || it.GetComponent() != null))) + target.HeldItems.Any(it => it.GetComponent() != null || it.GetComponent() != null)) { damage *= 0.5f; stun *= 0.5f; @@ -473,12 +493,12 @@ namespace Barotrauma { //cap the damage so the karma can't decrease by more than MaxStructureDamageKarmaDecreasePerSecond per second var clientMemory = GetClientMemory(client); - clientMemory.StructureDamageAccumulator += damageAmount; if (clientMemory.StructureDamagePerSecond + damageAmount >= MaxStructureDamageKarmaDecreasePerSecond / StructureDamageKarmaDecrease) { - damageAmount -= (MaxStructureDamageKarmaDecreasePerSecond / StructureDamageKarmaDecrease) - clientMemory.StructureDamagePerSecond; + damageAmount -= (clientMemory.StructureDamagePerSecond + damageAmount) - (MaxStructureDamageKarmaDecreasePerSecond / StructureDamageKarmaDecrease); if (damageAmount <= 0.0f) { return; } } + clientMemory.StructureDamageAccumulator += damageAmount; } AdjustKarma(attacker, -damageAmount * StructureDamageKarmaDecrease, "Damaged structures"); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs index 85ce04c9a..bfabe7aa8 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs @@ -233,7 +233,7 @@ namespace Barotrauma.Networking case ConnectionInitialization.ContentPackageOrder: outMsg.Write(GameMain.Server.ServerName); - var mpContentPackages = GameMain.Config.AllEnabledPackages.Where(cp => cp.HasMultiplayerIncompatibleContent).Reverse().ToList(); + var mpContentPackages = GameMain.Config.AllEnabledPackages.Where(cp => cp.HasMultiplayerIncompatibleContent).ToList(); outMsg.WriteVariableUInt32((UInt32)mpContentPackages.Count); for (int i = 0; i < mpContentPackages.Count; i++) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index be0d193bd..4cf089309 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -36,7 +36,7 @@ namespace Barotrauma.Networking if (GameMain.Server.ServerSettings.BotSpawnMode == BotSpawnMode.Normal) { return Character.CharacterList - .FindAll(c => c.TeamID == Character.TeamType.Team1 && c.AIController != null && c.Info != null && c.IsDead) + .FindAll(c => c.TeamID == CharacterTeamType.Team1 && c.AIController != null && c.Info != null && c.IsDead) .Select(c => c.Info) .ToList(); } @@ -46,7 +46,7 @@ namespace Barotrauma.Networking (!c.SpectateOnly || (!GameMain.Server.ServerSettings.AllowSpectating && GameMain.Server.OwnerConnection != c.Connection))); var existingBots = Character.CharacterList - .FindAll(c => c.TeamID == Character.TeamType.Team1 && c.AIController != null && c.Info != null); + .FindAll(c => c.TeamID == CharacterTeamType.Team1 && c.AIController != null && c.Info != null); int requiredBots = GameMain.Server.ServerSettings.BotCount - currPlayerCount; requiredBots -= existingBots.Count(b => !b.IsDead); @@ -238,13 +238,17 @@ namespace Barotrauma.Networking //all characters are in Team 1 in game modes/missions with only one team. //if at some point we add a game mode with multiple teams where respawning is possible, this needs to be reworked - c.TeamID = Character.TeamType.Team1; + c.TeamID = CharacterTeamType.Team1; if (c.CharacterInfo == null) { c.CharacterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, c.Name); } } List characterInfos = clients.Select(c => c.CharacterInfo).ToList(); - var botsToSpawn = GetBotsToRespawn(); - characterInfos.AddRange(botsToSpawn); + //bots don't respawn in the campaign + if (campaign == null) + { + var botsToSpawn = GetBotsToRespawn(); + characterInfos.AddRange(botsToSpawn); + } GameMain.Server.AssignJobs(clients); foreach (Client c in clients) @@ -276,7 +280,7 @@ namespace Barotrauma.Networking characterInfos[i].CurrentOrderOption = null; var character = Character.Create(characterInfos[i], shuttleSpawnPoints[i].WorldPosition, characterInfos[i].Name, isRemotePlayer: !bot, hasAi: bot); - character.TeamID = Character.TeamType.Team1; + character.TeamID = CharacterTeamType.Team1; if (bot) { @@ -348,9 +352,9 @@ namespace Barotrauma.Networking } //add the ID card tags they should've gotten when spawning in the shuttle - foreach (Item item in character.Inventory.Items) + foreach (Item item in character.Inventory.AllItems.Distinct()) { - if (item == null || item.Prefab.Identifier != "idcard") { continue; } + if (item.Prefab.Identifier != "idcard") { continue; } foreach (string s in shuttleSpawnPoints[i].IdCardTags) { item.AddTag(s); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFindItem.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFindItem.cs index 90b41c821..f13cb0e1d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFindItem.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFindItem.cs @@ -180,7 +180,7 @@ namespace Barotrauma if (allowNew && !targetContainer.OwnInventory.IsFull()) { existingItems.Clear(); - foreach (var item in targetContainer.OwnInventory.Items) + foreach (var item in targetContainer.OwnInventory.AllItems.Distinct()) { existingItems.Add(item); } @@ -205,7 +205,7 @@ namespace Barotrauma base.Update(deltaTime); if (target == null) { - target = targetContainer.OwnInventory.Items.FirstOrDefault(item => item != null && item.Prefab.Identifier == (containedPrefab != null ? itemContainerId : identifier) && !existingItems.Contains(item)); + target = targetContainer.ContainedItems.FirstOrDefault(item => item.Prefab.Identifier == (containedPrefab != null ? itemContainerId : identifier) && !existingItems.Contains(item)); if (target != null) { if (containedPrefab != null) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalReplaceInventory.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalReplaceInventory.cs index 7df50ea26..3ffb3953c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalReplaceInventory.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalReplaceInventory.cs @@ -36,7 +36,7 @@ namespace Barotrauma if (sabotageContainerIds.Contains(item.prefab.Identifier)) { ++totalAmount; - if (item.OwnInventory.Items.Length <= 0 || item.OwnInventory.Items.All(containedItem => containedItem != null && !validReplacementIds.Contains(containedItem.Prefab.Identifier))) + if (item.OwnInventory.AllItems.All(containedItem => !validReplacementIds.Contains(containedItem.Prefab.Identifier))) { continue; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs index a9eb9cb52..49e44f615 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorManager.cs @@ -19,10 +19,10 @@ namespace Barotrauma // All traitor related functionality should use the following interface for generating random values public static double RandomDouble() => Random.NextDouble(); - public readonly Dictionary Missions = new Dictionary(); + public readonly Dictionary Missions = new Dictionary(); - public string GetCodeWords(Character.TeamType team) => Missions.TryGetValue(team, out var mission) ? mission.CodeWords : ""; - public string GetCodeResponse(Character.TeamType team) => Missions.TryGetValue(team, out var mission) ? mission.CodeResponse : ""; + public string GetCodeWords(CharacterTeamType team) => Missions.TryGetValue(team, out var mission) ? mission.CodeWords : ""; + public string GetCodeResponse(CharacterTeamType team) => Missions.TryGetValue(team, out var mission) ? mission.CodeResponse : ""; public IEnumerable Traitors => Missions.Values.SelectMany(mission => mission.Traitors.Values); @@ -87,18 +87,18 @@ namespace Barotrauma { bool missionCompleted = false; bool gameShouldEnd = false; - Character.TeamType winningTeam = Character.TeamType.None; + CharacterTeamType winningTeam = CharacterTeamType.None; foreach (var mission in Missions) { mission.Value.Update(deltaTime, () => { switch (mission.Key) { - case Character.TeamType.Team1: - winningTeam = (winningTeam == Character.TeamType.None) ? Character.TeamType.Team2 : Character.TeamType.None; + case CharacterTeamType.Team1: + winningTeam = (winningTeam == CharacterTeamType.None) ? CharacterTeamType.Team2 : CharacterTeamType.None; break; - case Character.TeamType.Team2: - winningTeam = (winningTeam == Character.TeamType.None) ? Character.TeamType.Team1 : Character.TeamType.None; + case CharacterTeamType.Team2: + winningTeam = (winningTeam == CharacterTeamType.None) ? CharacterTeamType.Team1 : CharacterTeamType.None; break; default: break; @@ -137,13 +137,13 @@ namespace Barotrauma startCountdown = MathHelper.Lerp(server.ServerSettings.TraitorsMinRestartDelay, server.ServerSettings.TraitorsMaxRestartDelay, (float)RandomDouble()); return; } - if (Character.CharacterList.Count(c => !c.IsDead && c.TeamID == Character.TeamType.Team1 || c.TeamID == Character.TeamType.Team2) <= 1) + if (Character.CharacterList.Count(c => !c.IsDead && c.TeamID == CharacterTeamType.Team1 || c.TeamID == CharacterTeamType.Team2) <= 1) { return; } if (GameMain.GameSession.Mission is CombatMission) { - var teamIds = new[] { Character.TeamType.Team1, Character.TeamType.Team2 }; + var teamIds = new[] { CharacterTeamType.Team1, CharacterTeamType.Team2 }; foreach (var teamId in teamIds) { if (server.ConnectedClients.Count(c => c.Character != null && !c.Character.IsDead && c.TeamID == teamId) < 2) @@ -170,11 +170,11 @@ namespace Barotrauma { var mission = TraitorMissionPrefab.RandomPrefab()?.Instantiate(); if (mission != null) { - if (mission.CanBeStarted(server, this, Character.TeamType.None)) + if (mission.CanBeStarted(server, this, CharacterTeamType.None)) { - if (mission.Start(server, this, Character.TeamType.None)) + if (mission.Start(server, this, CharacterTeamType.None)) { - Missions.Add(Character.TeamType.None, mission); + Missions.Add(CharacterTeamType.None, mission); return; } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMission.cs index d51c0f9da..f340ab805 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMission.cs @@ -87,13 +87,13 @@ namespace Barotrauma return pendingObjectives.Find(objective => objective.Roles.Contains(traitor.Role)); } - protected List> FindTraitorCandidates(GameServer server, Character.TeamType team, RoleFilter traitorRoleFilter) + protected List> FindTraitorCandidates(GameServer server, CharacterTeamType team, RoleFilter traitorRoleFilter) { var traitorCandidates = new List>(); foreach (Client c in server.ConnectedClients) { if (c.Character == null || c.Character.IsDead || c.Character.Removed || !traitorRoleFilter(c.Character) || - (team != Character.TeamType.None && c.Character.TeamID != team)) + (team != CharacterTeamType.None && c.Character.TeamID != team)) { continue; } @@ -115,7 +115,7 @@ namespace Barotrauma return characters; } - protected List>> AssignTraitors(GameServer server, TraitorManager traitorManager, Character.TeamType team) + protected List>> AssignTraitors(GameServer server, TraitorManager traitorManager, CharacterTeamType team) { List characters = FindCharacters(); #if !ALLOW_SOLO_TRAITOR @@ -176,7 +176,7 @@ namespace Barotrauma return assignedCandidates; } - public bool CanBeStarted(GameServer server, TraitorManager traitorManager, Character.TeamType team) + public bool CanBeStarted(GameServer server, TraitorManager traitorManager, CharacterTeamType team) { foreach (var role in Roles) { @@ -189,7 +189,7 @@ namespace Barotrauma return AssignTraitors(server, traitorManager, team) != null; } - public bool Start(GameServer server, TraitorManager traitorManager, Character.TeamType team) + public bool Start(GameServer server, TraitorManager traitorManager, CharacterTeamType team) { var assignedCandidates = AssignTraitors(server, traitorManager, team); if (assignedCandidates == null) diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 8cd1d75e5..31c73c5ca 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.11.0.9 + 0.12.0.2 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 0fe12a622..b90e7b764 100644 --- a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml +++ b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml @@ -64,15 +64,12 @@ - - - @@ -88,7 +85,6 @@ - @@ -100,6 +96,13 @@ + + + + + + + @@ -161,7 +164,11 @@ - + + + + + diff --git a/Barotrauma/BarotraumaShared/Data/forbiddenwordlist.txt b/Barotrauma/BarotraumaShared/Data/forbiddenwordlist.txt index 7e3c1430c..81cbd5d65 100644 --- a/Barotrauma/BarotraumaShared/Data/forbiddenwordlist.txt +++ b/Barotrauma/BarotraumaShared/Data/forbiddenwordlist.txt @@ -1,57 +1,427 @@ +4r5e +5h1t +5hit +Dumbcunt +a$$ +a$$hole +a2m +a54 +a55 +a55hole +a_s_s adolf -anal +ahole + anal anus anuses -ass +arrse +arse + ass asses +asshat +asshole +assshole +b!tch +b17ch +b1tch +ballsack +beatch bitch +bitchass +bitched +bitcher +bitchers bitches +bitches +bitching +bitchy blowjob blowjobs +bulldyke +bullshit +bullshits +bullshitted +bullturds +bung +bunghole +buttfucker +butthole +buttmuch +buttmunch +c-0-c-k +c.0.c.k +c.o.c.k. +c.u.n.t +c0ck +carpetmuncher + chinc +chincs +chink +chinky clitoris -cock + cock +cock sucker +cock-sucker cocks +cocksucker +cocksuckers +cocksucking +cocksucks +coochie +coochy + coon +coonnass + coons +cracker +crackwhore +crap +cumbubble +cumdump +cumdump +cumdumpster +cumguzzler +cumjockey +cummer +cummin +cumming + cums +cumshot +cumshots +cumslut +cumstain +cumtart +cunilingus +cunillingus +cunnie +cunnilingus + cunny cunt +cuntass +cuntbag +cuntbag +cuntface +cunthole +cunthunter +cuntlick +cuntlick +cuntlicker +cuntlicker +cuntlicking +cuntlicking +cuntrag cunts +cunts +cuntsicle +cuntsicle +cuntslut +d0uch3 +d0uche +d1ck +d1ld0 +d1ldo +deepthroat dick +dick-ish +dick-sneeze +dickbag +dickbeaters +dickdipper +dickface +dickflipper +dickfuck +dickfucker +dickhead +dickheads +dickhole +dickish +dickjuice +dickmilk +dickmonger +dickripper dicks +dicks +dicksipper +dickslap +dicksucker +dicksucking +dicktickler +dickwad +dickweasel +dickweed +dickwhipper +dickwod +dickzipper +diddle +dike dildo dildos +douche +douchebag +dumbass +dumbasses +dumbfuck +dumbshit dyke dykes -gay -gays -fag -fags +ejaculate +ejaculated +ejaculates +ejaculates +ejaculating +ejaculating +ejaculatings +ejaculation +ejakulate +erect +f u c k +f u c k e r +f.u.c.k +f4nny +f_u_c_k + fag +fagbag +fagfucker +fagg +fagged +fagging +faggit +faggitt faggot +faggot* +faggotcock faggots +faggots +faggs +fagot +fagots + fags +fagtard +fatass +fcuk +fcuker +fcuking +feck +fecker +fistfuck +fistfucked +fistfucked +fistfucker +fistfucker +fistfuckers +fistfuckers +fistfucking +fistfucking +fistfuckings +fistfuckings +fistfucks +fistfucks fuck +fuck-ass +fuck ass +fuck-bitch +fuck bitch +fucktard +fuck tard +fucka +fuckass +fuckbag +fuckboy +fuckbrain +fuckbutt +fuckbutter +fucked +fuckedup +fucker +fuckers +fuckersucker +fuckface +fuckhead +fuckheads +fuckhole +fuckin +fucking +fuckings +fuckingshitmotherfucker +fuckme +fuckme +fuckmeat +fucknugget +fucknut +fucknutt +fuckoff +fucks +fuckstick +fucktard +fucktards +fucktart +fucktoy +fucktoy +fucktwat +fuckup +fuckwad +fuckwhit +fuckwit +fuckwitt +gay +gayass +gaybob +gaydo +gayfuck +gayfuckist +gaylord +gays +god-dam +god-damned +godamn +godamnit +goddam +goddammit +goddamn +goddamned +goddamnit +godsdamn hitler homo +homodumbshit +homoerotic +homoey homos +honkey +honky +jack-off +jackass +jackass +jackasses +jackasses +jackhole +jackhole +jackoff +jackoff +jaggi +jagoff +jailbait +jailbait + jap +japs + jerk +jerk-off +jerkoff +jerk off +jerk0ff +jerkass +jerked +jerkoff jew jews +jism +jiz +jiz +jizm +jizm +jizz +jizzed kike kikes +knob + kum +kummer +kumming +kums +lesbian +lesbians +lesbo +lesbos +lez +lezzie +master-bate +master-bate +masterbat* +masterbat3 +masterbate +masterbating +masterbation +masterbations +masturbate +masturbating +masturbation +mothafuck +mothafucka +mothafuckas +mothafuckaz +mothafucked +mothafucked +mothafucker +mothafuckers +mothafuckin +mothafucking +mothafuckings +mothafucks +motherfuck +motherfucka +motherfucked +motherfucker +motherfucker +motherfuckers +motherfuckin +motherfucking +motherfuckings +motherfuckka +motherfucks +mudslime* +mudslimes* nazi nazis -mudslime -mudslimes -nig + nig +nig-nog +nigg3r +nigg4h +nigga +nigga +niggah +niggas +niggas +niggaz +nigger nigger niggers -nigga -niggas +niggle +niglet +negroid +negroids penis -pussy +pigfucker + piss +piss-off +pissed +pisser +pissers +pisses +pisses +pissflaps +pissin +pissin +pissing +pissoff +pissoff pussies +pussy +queaf +queaf +queef queer queers + rape +raped +raper +rapey +raping +rapist slut sluts twat twats vagina vaginas +white power whore whores \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/README.txt b/Barotrauma/BarotraumaShared/README.txt index ac1e7c95a..a71341163 100644 --- a/Barotrauma/BarotraumaShared/README.txt +++ b/Barotrauma/BarotraumaShared/README.txt @@ -2,12 +2,12 @@ http://www.barotraumagame.com -© 2018-2019 FakeFish Ltd. All rights reserved. -© 2019 Daedalic Entertainment GmbH. The Daedalic logo is a trademark of Daedalic Entertainment GmbH, Germany. All rights reserved. +© 2018-2020 FakeFish Ltd. All rights reserved. +© 2019-2020 Daedalic Entertainment GmbH. The Daedalic logo is a trademark of Daedalic Entertainment GmbH, Germany. All rights reserved. Privacy policy: http://privacypolicy.daedalic.com See the wiki for more detailed info and instructions: -http://barotrauma.gamepedia.com +http://barotraumagame.com/wiki ------------------------------------------------------------------------ diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs index 6eb1cf197..bf34bd69a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs @@ -1,18 +1,17 @@ using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; +using Barotrauma.Items.Components; +using System.Linq; namespace Barotrauma { - public enum AIState { Idle, Attack, Escape, Eat, Flee, Avoid, Aggressive, PassiveAggressive, Protect, Observe, Freeze, Follow } - abstract partial class AIController : ISteerable { public bool Enabled; public readonly Character Character; - private AIState state; - // Update only when the value changes, not when it keeps the same. protected AITarget _lastAiTarget; // Updated each time the value is updated (also when the value is the same). @@ -74,25 +73,6 @@ namespace Barotrauma get { return true; } } - public virtual AIObjectiveManager ObjectiveManager - { - get { return null; } - } - - public AIState State - { - get { return state; } - set - { - if (state == value) { return; } - PreviousState = state; - OnStateChanged(state, value); - state = value; - } - } - - public AIState PreviousState { get; protected set; } - private IEnumerable visibleHulls; private float hullVisibilityTimer; const float hullVisibilityInterval = 0.5f; @@ -112,6 +92,9 @@ namespace Barotrauma } } + protected bool HasValidPath(bool requireNonDirty = false) => + steeringManager is IndoorsSteeringManager pathSteering && pathSteering.CurrentPath != null && !pathSteering.CurrentPath.Finished && !pathSteering.CurrentPath.Unreachable && (!requireNonDirty || !pathSteering.IsPathDirty); + public AIController (Character c) { Character = c; @@ -149,8 +132,152 @@ namespace Barotrauma public void FaceTarget(ISpatialEntity target) => Character.AnimController.TargetDir = target.WorldPosition.X > Character.WorldPosition.X ? Direction.Right : Direction.Left; + public bool IsSteeringThroughGap { get; protected set; } + + public virtual bool SteerThroughGap(Structure wall, WallSection section, Vector2 targetWorldPos, float deltaTime) + { + if (wall == null) { return false; } + if (section == null) { return false; } + Gap gap = section.gap; + if (gap == null) { return false; } + float maxDistance = Math.Min(wall.Rect.Width, wall.Rect.Height); + if (Vector2.DistanceSquared(Character.WorldPosition, targetWorldPos) > maxDistance * maxDistance) { return false; } + Hull targetHull = gap.FlowTargetHull; + if (targetHull == null) { return false; } + if (wall.IsHorizontal) + { + targetWorldPos.Y = targetHull.WorldRect.Y - targetHull.Rect.Height / 2; + } + else + { + targetWorldPos.X = targetHull.WorldRect.Center.X; + } + return SteerThroughGap(gap, targetWorldPos, deltaTime, maxDistance: -1); + } + + public virtual bool SteerThroughGap(Gap gap, Vector2 targetWorldPos, float deltaTime, float maxDistance = -1) + { + Hull targetHull = gap.FlowTargetHull; + if (targetHull == null) { return false; } + if (maxDistance > 0) + { + if (Vector2.DistanceSquared(Character.WorldPosition, targetWorldPos) > maxDistance * maxDistance) { return false; } + } + if (SteeringManager is IndoorsSteeringManager pathSteering) + { + pathSteering.ResetPath(); + } + SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(targetWorldPos - Character.WorldPosition)); + return true; + } + + public bool CanPassThroughHole(Structure wall, int sectionIndex, int requiredHoleCount) + { + if (!wall.SectionBodyDisabled(sectionIndex)) { return false; } + int holeCount = 1; + for (int j = sectionIndex - 1; j > sectionIndex - requiredHoleCount; j--) + { + if (wall.SectionBodyDisabled(j)) + { + holeCount++; + } + else + { + break; + } + } + for (int j = sectionIndex + 1; j < sectionIndex + requiredHoleCount; j++) + { + if (wall.SectionBodyDisabled(j)) + { + holeCount++; + } + else + { + break; + } + } + return holeCount >= requiredHoleCount; + } + + protected bool IsWallDisabled(Structure wall) + { + bool isDisabled = true; + for (int i = 0; i < wall.Sections.Length; i++) + { + if (!wall.SectionBodyDisabled(i)) + { + isDisabled = false; + break; + } + } + return isDisabled; + } + + private readonly HashSet unequippedItems = new HashSet(); + public bool TakeItem(Item item, Inventory targetInventory, bool equip, bool dropOtherIfCannotMove = true, bool allowSwapping = false, bool storeUnequipped = false) + { + var pickable = item.GetComponent(); + if (pickable == null) { return false; } + if (item.ParentInventory is ItemInventory itemInventory) + { + if (!itemInventory.Container.HasRequiredItems(Character, addMessage: false)) { return false; } + } + if (equip) + { + int targetSlot = -1; + //check if all the slots required by the item are free + foreach (InvSlotType slots in pickable.AllowedSlots) + { + if (slots.HasFlag(InvSlotType.Any)) { continue; } + for (int i = 0; i < targetInventory.Capacity; i++) + { + if (targetInventory is CharacterInventory characterInventory) + { + //slot not needed by the item, continue + if (!slots.HasFlag(characterInventory.SlotTypes[i])) { continue; } + } + targetSlot = i; + //slot free, continue + var otherItem = targetInventory.GetItemAt(i); + if (otherItem == null) { continue; } + //try to move the existing item to LimbSlot.Any and continue if successful + if (otherItem.AllowedSlots.Contains(InvSlotType.Any) && targetInventory.TryPutItem(otherItem, Character, CharacterInventory.anySlot)) + { + if (storeUnequipped && targetInventory.Owner == Character) + { + unequippedItems.Add(otherItem); + } + continue; + } + if (dropOtherIfCannotMove) + { + //if everything else fails, simply drop the existing item + otherItem.Drop(Character); + } + } + } + return targetInventory.TryPutItem(item, targetSlot, allowSwapping, allowCombine: false, Character); + } + else + { + return targetInventory.TryPutItem(item, Character, CharacterInventory.anySlot); + } + } + + public void ReequipUnequipped() + { + foreach (var item in unequippedItems) + { + if (item != null && !item.Removed && Character.HasItem(item)) + { + TakeItem(item, Character.Inventory, equip: true, dropOtherIfCannotMove: true, allowSwapping: true, storeUnequipped: false); + } + } + unequippedItems.Clear(); + } + protected virtual void OnStateChanged(AIState from, AIState to) { } protected virtual void OnTargetChanged(AITarget previousTarget, AITarget newTarget) { } - } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index bcb3b4ebe..250c2f770 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -10,30 +10,32 @@ using System.Linq; namespace Barotrauma { + public enum AIState { Idle, Attack, Escape, Eat, Flee, Avoid, Aggressive, PassiveAggressive, Protect, Observe, Freeze, Follow } + partial class EnemyAIController : AIController { public static bool DisableEnemyAI; + private AIState _state; + public AIState State + { + get { return _state; } + set + { + if (_state == value) { return; } + PreviousState = _state; + OnStateChanged(_state, value); + _state = value; + } + } + + public AIState PreviousState { get; private set; } + /// /// Enable the character to attack the outposts and the characters inside them. Disabled by default in normal levels, enabled in outpost levels. /// public bool TargetOutposts; - // TODO: use a struct? - class WallTarget - { - public Vector2 Position; - public Structure Structure; - public int SectionIndex; - - public WallTarget(Vector2 position, Structure structure = null, int sectionIndex = -1) - { - Position = position; - Structure = structure; - SectionIndex = sectionIndex; - } - } - private readonly float updateTargetsInterval = 1; private readonly float updateMemoriesInverval = 1; private readonly float attackLimbResetInterval = 2; @@ -56,9 +58,6 @@ namespace Barotrauma private FishAnimController FishAnimController => Character.AnimController as FishAnimController; - //a point in a wall which the Character is currently targeting - private WallTarget wallTarget; - //the limb selected for the current attack private Limb _attackingLimb; public Limb AttackingLimb @@ -159,6 +158,11 @@ namespace Barotrauma } } + private readonly float maxSteeringBuffer = 5000; + private readonly float minSteeringBuffer = 500; + private readonly float steeringBufferIncreaseSpeed = 100; + private float steeringBuffer; + public EnemyAIController(Character c, string seed) : base(c) { if (c.IsHuman) @@ -168,10 +172,9 @@ namespace Barotrauma if (Character.Params.Group.Equals("human", StringComparison.OrdinalIgnoreCase)) { // Pet - Character.TeamID = Character.TeamType.FriendlyNPC; + Character.TeamID = CharacterTeamType.FriendlyNPC; } - CharacterPrefab prefab = CharacterPrefab.FindBySpeciesName(c.SpeciesName); - var mainElement = prefab.XDocument.Root.IsOverride() ? prefab.XDocument.Root.FirstElement() : prefab.XDocument.Root; + var mainElement = c.Params.OriginalElement.IsOverride() ? c.Params.OriginalElement.FirstElement() : c.Params.OriginalElement; targetMemories = new Dictionary(); steeringManager = outsideSteering; //allow targeting outposts and outpost NPCs in outpost levels @@ -188,7 +191,7 @@ namespace Barotrauma if (aiElements.Count == 0) { - DebugConsole.ThrowError("Error in file \"" + prefab.FilePath + "\" - no AI element found."); + DebugConsole.ThrowError("Error in file \"" + c.Params.File + "\" - no AI element found."); outsideSteering = new SteeringManager(this); insideSteering = new IndoorsSteeringManager(this, false, false); return; @@ -229,7 +232,7 @@ namespace Barotrauma ReevaluateAttacks(); outsideSteering = new SteeringManager(this); - insideSteering = new IndoorsSteeringManager(this, false, canAttackDoors); + insideSteering = new IndoorsSteeringManager(this, Character.IsHumanoid, canAttackDoors); steeringManager = outsideSteering; State = AIState.Idle; @@ -242,7 +245,24 @@ namespace Barotrauma myBodies = Character.AnimController.Limbs.Select(l => l.body.FarseerBody); } - public CharacterParams.AIParams AIParams => Character.Params.AI; + private CharacterParams.AIParams _aiParams; + public CharacterParams.AIParams AIParams + { + get + { + if (_aiParams == null) + { + _aiParams = Character.Params.AI; + if (_aiParams == null) + { + DebugConsole.ThrowError($"No AI Params defined for {Character.SpeciesName}. AI disabled."); + Enabled = false; + _aiParams = new CharacterParams.AIParams(null, Character.Params); + } + } + return _aiParams; + } + } private CharacterParams.TargetParams GetTargetParams(string targetTag) => AIParams.GetTarget(targetTag, false); private CharacterParams.TargetParams GetTargetParams(AITarget aiTarget) => GetTargetParams(GetTargetingTag(aiTarget)); private string GetTargetingTag(AITarget aiTarget) @@ -321,7 +341,7 @@ namespace Barotrauma } private float movementMargin; - + public override void Update(float deltaTime) { if (DisableEnemyAI) { return; } @@ -341,14 +361,23 @@ namespace Barotrauma ignorePlatforms = height < allowedJumpHeight; } } + if (Character.IsClimbing && PathSteering.IsNextLadderSameAsCurrent) + { + Character.AnimController.TargetMovement = new Vector2(0.0f, Math.Sign(Character.AnimController.TargetMovement.Y)); + } } Character.AnimController.IgnorePlatforms = ignorePlatforms; - //clients get the facing direction from the server - if (Character.AnimController is HumanoidAnimController && + if (Math.Abs(Character.AnimController.movement.X) > 0.1f && !Character.AnimController.InWater && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer || Character.Controlled == Character)) { - if (Math.Abs(Character.AnimController.movement.X) > 0.1f && !Character.AnimController.InWater) + if (SelectedAiTarget?.Entity != null || escapeTarget != null) + { + Entity t = SelectedAiTarget?.Entity ?? escapeTarget; + float referencePos = Vector2.DistanceSquared(Character.WorldPosition, t.WorldPosition) > 100 * 100 && HasValidPath(true) ? PathSteering.CurrentPath.CurrentNode.WorldPosition.X : t.WorldPosition.X; + Character.AnimController.TargetDir = Character.WorldPosition.X < referencePos ? Direction.Right : Direction.Left; + } + else { Character.AnimController.TargetDir = Character.AnimController.movement.X > 0.0f ? Direction.Right : Direction.Left; } @@ -389,7 +418,7 @@ namespace Barotrauma FadeMemories(updateMemoriesInverval); updateMemoriesTimer = updateMemoriesInverval; } - if (Character.HealthPercentage <= FleeHealthThreshold && SelectedAiTarget != null && + if (Math.Max(Character.HealthPercentage, 0) < FleeHealthThreshold && SelectedAiTarget != null && SelectedAiTarget.Entity is Character target && (target.IsHuman && CanPerceive(SelectedAiTarget) || IsBeingChasedBy(target))) { // Keep fleeing if being chased @@ -408,7 +437,7 @@ namespace Barotrauma UpdateTargets(Character, out targetingParams); if (!IsLatchedOnSub) { - UpdateWallTarget(); + UpdateWallTarget(requiredHoleCount); } updateTargetsTimer = updateTargetsInterval * Rand.Range(0.75f, 1.25f); if (SelectedAiTarget == null) @@ -423,21 +452,48 @@ namespace Barotrauma } } - if (Character.CurrentHull == null) + if (AIParams.Infiltrate) { - if (steeringManager != outsideSteering) + bool IsCloseEnoughToTargetSub(float threshold) => SelectedAiTarget?.Entity?.Submarine is Submarine sub && sub != null && Vector2.DistanceSquared(Character.WorldPosition, sub.WorldPosition) < MathUtils.Pow(Math.Max(sub.Borders.Size.X, sub.Borders.Size.Y) / 2 + threshold, 2); + + if (Character.Submarine != null || HasValidPath() && IsCloseEnoughToTargetSub(maxSteeringBuffer) || IsCloseEnoughToTargetSub(steeringBuffer)) { - outsideSteering.Reset(); + if (steeringManager != insideSteering) + { + insideSteering.Reset(); + } + steeringManager = insideSteering; + steeringBuffer += steeringBufferIncreaseSpeed * deltaTime; } - steeringManager = outsideSteering; + else + { + if (steeringManager != outsideSteering) + { + outsideSteering.Reset(); + } + steeringManager = outsideSteering; + steeringBuffer = minSteeringBuffer; + } + steeringBuffer = Math.Clamp(steeringBuffer, minSteeringBuffer, maxSteeringBuffer); } else { - if (steeringManager != insideSteering) + if (Character.Submarine != null) { - insideSteering.Reset(); + if (steeringManager != insideSteering) + { + insideSteering.Reset(); + } + steeringManager = insideSteering; + } + else + { + if (steeringManager != outsideSteering) + { + outsideSteering.Reset(); + } + steeringManager = outsideSteering; } - steeringManager = insideSteering; } bool useSteeringLengthAsMovementSpeed = State == AIState.Idle && Character.AnimController.InWater; @@ -519,12 +575,24 @@ namespace Barotrauma } if (State == AIState.Protect) { - if (SelectedAiTarget.Entity is Character targetCharacter && targetCharacter.LastAttacker is Character attacker && !attacker.Removed && !attacker.IsDead) + if (SelectedAiTarget.Entity is Character targetCharacter) { - // Attack the character that attacked the target we are protecting - ChangeTargetState(attacker, AIState.Attack, selectedTargetingParams.Priority * 2); - SelectTarget(attacker.AiTarget); - return; + bool IsValid(Character.Attacker a) + { + Character c = a.Character; + if (c.IsDead || c.Removed) { return false; } + if (!IsFriendly(Character, c)) { return true; } + // Only apply the threshold to friendly characters + return a.Damage >= selectedTargetingParams.Threshold; + } + Character attacker = targetCharacter.LastAttackers.LastOrDefault(IsValid)?.Character; + if (attacker != null) + { + // Attack the character that attacked the target we are protecting + ChangeTargetState(attacker, AIState.Attack, selectedTargetingParams.Priority * 2); + SelectTarget(attacker.AiTarget); + return; + } } } float sqrDist = Vector2.DistanceSquared(WorldPosition, SelectedAiTarget.WorldPosition); @@ -622,8 +690,8 @@ namespace Barotrauma Character.AnimController.TargetMovement = Character.ApplyMovementLimits(Steering, targetMovement); if (Character.CurrentHull != null && Character.AnimController.InWater) { - // Halve the swimming speed inside the sub - Character.AnimController.TargetMovement *= 0.5f; + // Limit the swimming speed inside the sub. + Character.AnimController.TargetMovement = Character.AnimController.TargetMovement.ClampLength(5); } } @@ -691,6 +759,8 @@ namespace Barotrauma #endregion #region Escape + private readonly float escapeTargetSeekInterval = 2; + private float escapeTimer; private Gap escapeTarget; private bool allGapsSearched; private readonly HashSet unreachableGaps = new HashSet(); @@ -707,35 +777,62 @@ namespace Barotrauma } IndoorsSteeringManager pathSteering = SteeringManager as IndoorsSteeringManager; bool hasValidPath = pathSteering?.CurrentPath != null && !pathSteering.IsPathDirty && !pathSteering.CurrentPath.Unreachable; + if (allGapsSearched) + { + escapeTimer -= deltaTime; + if (escapeTimer <= 0) + { + allGapsSearched = false; + } + } if (Character.CurrentHull != null && pathSteering != null) { // Seek exit if inside if (!allGapsSearched) { + float closestDistance = 0; foreach (Gap gap in Gap.GapList) { if (gap == null || gap.Removed) { continue; } if (escapeTarget == gap) { continue; } if (unreachableGaps.Contains(gap)) { continue; } if (gap.Submarine != Character.Submarine) { continue; } - if (gap.Open < 1 || gap.IsRoomToRoom) { continue; } - bool canGetThrough = ConvertUnits.ToDisplayUnits(colliderWidth) < gap.Size; - if (!canGetThrough) { continue; } - if (escapeTarget == null) + if (gap.IsRoomToRoom) { continue; } + float multiplier = 1; + var door = gap.ConnectedDoor; + if (door != null) { - escapeTarget = gap; + if (!door.CanBeTraversed) + { + if (!door.HasAccess(Character)) + { + if (!canAttackDoors) { continue; } + // Treat doors that don't have access to like they were farther, because it will take time to break them. + multiplier = 5; + } + } } - else if (gap.FlowTargetHull == Character.CurrentHull) + else { + if (gap.Open < 1) { continue; } + bool canGetThrough = ConvertUnits.ToDisplayUnits(colliderWidth) < gap.Size; + if (!canGetThrough) { continue; } + } + if (gap.FlowTargetHull == Character.CurrentHull) + { + // If the gap is in the same room, it's close enough. escapeTarget = gap; break; } - else if (Vector2.DistanceSquared(Character.SimPosition, gap.SimPosition) < Vector2.DistanceSquared(Character.SimPosition, escapeTarget.SimPosition)) + float distance = Vector2.DistanceSquared(Character.WorldPosition, gap.WorldPosition) * multiplier; + if (escapeTarget == null || distance < closestDistance) { escapeTarget = gap; + closestDistance = distance; } } allGapsSearched = true; + escapeTimer = escapeTargetSeekInterval; } else if (escapeTarget != null && escapeTarget.FlowTargetHull != Character.CurrentHull) { @@ -760,36 +857,25 @@ namespace Barotrauma Vector2 escapeDir = Vector2.Normalize(SelectedAiTarget != null ? WorldPosition - SelectedAiTarget.WorldPosition : Character.AnimController.TargetMovement); if (!MathUtils.IsValid(escapeDir)) { escapeDir = Vector2.UnitY; } SteeringManager.SteeringManual(deltaTime, escapeDir); + return; } else if (pathSteering != null) { - if (canAttackDoors && hasValidPath) + if (hasValidPath && canAttackDoors) { var door = pathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? pathSteering.CurrentPath.NextNode?.ConnectedDoor; - if (door != null && !door.IsOpen && !door.IsBroken) + if (door != null && !door.CanBeTraversed && !door.HasAccess(Character)) { - if (SelectedAiTarget != door.Item.AiTarget) + if (SelectedAiTarget != door.Item.AiTarget || State != AIState.Attack) { - SelectTarget(door.Item.AiTarget); + SelectTarget(door.Item.AiTarget, selectedTargetMemory.Priority); State = AIState.Attack; return; } } - else - { - SteeringManager.SteeringSeek(escapeTarget.SimPosition, 5); - } - } - else - { - SteeringManager.SteeringSeek(escapeTarget.SimPosition, 5); } } - else - { - SteeringManager.SteeringSeek(escapeTarget.SimPosition, 10); - SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15); - } + SteeringManager.SteeringSeek(escapeTarget.SimPosition, 10); } else { @@ -860,49 +946,9 @@ namespace Barotrauma if (Character.AnimController.CanEnterSubmarine) { - //targeting a wall section that can be passed through -> steer manually through the hole - if (wallTarget != null && wallTarget.SectionIndex > -1 && CanPassThroughHole(wallTarget.Structure, wallTarget.SectionIndex)) + if (TrySteerThroughGaps(deltaTime)) { - WallSection section = wallTarget.Structure.GetSection(wallTarget.SectionIndex); - Vector2 targetPos = wallTarget.Structure.SectionPosition(wallTarget.SectionIndex, true); - if (section?.gap != null && SteerThroughGap(wallTarget.Structure, section, targetPos, deltaTime)) - { - return; - } - } - else if (SelectedAiTarget.Entity is Structure wall) - { - for (int i = 0; i < wall.Sections.Length; i++) - { - WallSection section = wall.Sections[i]; - if (CanPassThroughHole(wall, i) && section?.gap != null) - { - if (SteerThroughGap(wall, section, wall.SectionPosition(i, true), deltaTime)) - { - return; - } - } - } - } - else if (SelectedAiTarget.Entity is Item i) - { - var door = i.GetComponent(); - // Steer through the door manually if it's open or broken - // Don't try to enter dry hulls if cannot walk or if the gap is too narrow - if (door?.LinkedGap?.FlowTargetHull != null && !door.LinkedGap.IsRoomToRoom && (door.IsOpen || door.IsBroken)) - { - if (Character.AnimController.CanWalk || door.LinkedGap.FlowTargetHull.WaterPercentage > 25) - { - if (door.LinkedGap.Size > ConvertUnits.ToDisplayUnits(colliderWidth)) - { - LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 2); - Character.AnimController.ReleaseStuckLimbs(); - var velocity = Vector2.Normalize(door.LinkedGap.FlowTargetHull.WorldPosition - Character.WorldPosition); - steeringManager.SteeringManual(deltaTime, velocity); - return; - } - } - } + return; } } else if (SelectedAiTarget.Entity is Structure w && wallTarget == null) @@ -932,12 +978,6 @@ namespace Barotrauma } } - if (Math.Abs(Character.AnimController.movement.X) > 0.1f && !Character.AnimController.InWater && - (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer || Character.Controlled == Character)) - { - Character.AnimController.TargetDir = Character.WorldPosition.X < attackWorldPos.X ? Direction.Right : Direction.Left; - } - bool canAttack = true; bool pursue = false; if (IsCoolDownRunning) @@ -1119,30 +1159,34 @@ namespace Barotrauma } canAttack = AttackingLimb != null && AttackingLimb.attack.CoolDownTimer <= 0; } - if (!Character.AnimController.SimplePhysicsEnabled && SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null && (!canAttackDoors || !canAttackWalls || !AIParams.TargetOuterWalls)) + if (!AIParams.Infiltrate) { - if (wallTarget == null && Vector2.DistanceSquared(Character.WorldPosition, attackWorldPos) < 2000 * 2000) + if (!Character.AnimController.SimplePhysicsEnabled && SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null && (!canAttackDoors || !canAttackWalls || !AIParams.TargetOuterWalls)) { - // Check that we are not bumping into a door or a wall - Vector2 rayStart = SimPosition; - if (Character.Submarine == null) + if (wallTarget == null && Vector2.DistanceSquared(Character.WorldPosition, attackWorldPos) < 2000 * 2000) { - rayStart -= SelectedAiTarget.Entity.Submarine.SimPosition; - } - Vector2 dir = SelectedAiTarget.WorldPosition - WorldPosition; - Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 2); - Body closestBody = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true); - if (Submarine.LastPickedFraction != 1.0f && closestBody != null && - (!AIParams.TargetOuterWalls || !canAttackWalls && closestBody.UserData is Structure s && s.Submarine != null || !canAttackDoors && closestBody.UserData is Item i && i.Submarine != null && i.GetComponent() != null)) - { - // Target is unreachable, there's a door or wall ahead - State = AIState.Idle; - IgnoreTarget(SelectedAiTarget); - ResetAITarget(); - return; + // Check that we are not bumping into a door or a wall + Vector2 rayStart = SimPosition; + if (Character.Submarine == null) + { + rayStart -= SelectedAiTarget.Entity.Submarine.SimPosition; + } + Vector2 dir = SelectedAiTarget.WorldPosition - WorldPosition; + Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 2); + Body closestBody = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true); + if (Submarine.LastPickedFraction != 1.0f && closestBody != null && + (!AIParams.TargetOuterWalls || !canAttackWalls && closestBody.UserData is Structure s && s.Submarine != null || !canAttackDoors && closestBody.UserData is Item i && i.Submarine != null && i.GetComponent() != null)) + { + // Target is unreachable, there's a door or wall ahead + State = AIState.Idle; + IgnoreTarget(SelectedAiTarget); + ResetAITarget(); + return; + } } } } + float distance = 0; Limb attackTargetLimb = null; Character targetCharacter = SelectedAiTarget.Entity as Character; @@ -1202,6 +1246,16 @@ namespace Barotrauma // Check that we can reach the target distance = toTarget.Length(); canAttack = distance < AttackingLimb.attack.Range; + + // Crouch if the target is down (only humanoids), so that we can reach it. + if (Character.AnimController is HumanoidAnimController humanoidAnimController && distance < AttackingLimb.attack.Range * 2) + { + if (Math.Abs(toTarget.Y) > AttackingLimb.attack.Range / 2 && Math.Abs(toTarget.X) <= AttackingLimb.attack.Range) + { + humanoidAnimController.Crouching = true; + } + } + if (canAttack) { if (AttackingLimb.attack.Ranged) @@ -1303,26 +1357,27 @@ namespace Barotrauma if (targetCharacter == null || targetCharacter.CurrentHull != Character.CurrentHull) { var door = pathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? pathSteering.CurrentPath.NextNode?.ConnectedDoor; - if (door != null && !door.IsOpen && !door.IsBroken) + if (door != null && !door.CanBeTraversed && !door.HasAccess(Character)) { if (door.Item.AiTarget != null && SelectedAiTarget != door.Item.AiTarget) { SelectTarget(door.Item.AiTarget, selectedTargetMemory.Priority); + State = AIState.Attack; return; } } } } // Steer towards the target if in the same room and swimming - if ((Character.AnimController.InWater || pursue || !Character.AnimController.CanWalk) && - (targetCharacter != null && VisibleHulls.Contains(targetCharacter.CurrentHull) || Character.CanSeeTarget(SelectedAiTarget.Entity))) + if (Character.CurrentHull != null && ((Character.AnimController.InWater || pursue || !Character.AnimController.CanWalk) && + (targetCharacter != null && VisibleHulls.Contains(targetCharacter.CurrentHull)))) { Vector2 myPos = Character.AnimController.SimplePhysicsEnabled ? Character.SimPosition : steeringLimb.SimPosition; SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(steerPos - myPos)); } else { - SteeringManager.SteeringSeek(steerPos, 2); + pathSteering.SteeringSeek(steerPos, 2, startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (Character.CurrentHull == null), checkVisiblity: true); // Switch to Idle when cannot reach the target and if cannot damage the walls if ((!canAttackWalls || wallTarget == null) && !pathSteering.IsPathDirty && pathSteering.CurrentPath.Unreachable) { @@ -1363,6 +1418,16 @@ namespace Barotrauma } if (canAttack) { + if (SelectedAiTarget.Entity is Item targetItem) + { + var door = targetItem.GetComponent(); + if (door != null && door.CanBeTraversed) + { + ResetAITarget(); + State = PreviousState; + return; + } + } if (!UpdateLimbAttack(deltaTime, AttackingLimb, attackSimPos, distance, attackTargetLimb)) { IgnoreTarget(SelectedAiTarget); @@ -1370,38 +1435,6 @@ namespace Barotrauma } } - public bool IsSteeringThroughGap { get; private set; } - private bool SteerThroughGap(Structure wall, WallSection section, Vector2 targetWorldPos, float deltaTime) - { - IsSteeringThroughGap = true; - wallTarget = null; - LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 2); - Character.AnimController.ReleaseStuckLimbs(); - Hull targetHull = section.gap?.FlowTargetHull; - float maxDistance = Math.Min(wall.Rect.Width, wall.Rect.Height); - if (Vector2.DistanceSquared(Character.WorldPosition, targetWorldPos) > maxDistance * maxDistance) - { - return false; - } - if (targetHull != null) - { - // If already inside, target the hull, else target the wall. - SelectedAiTarget = Character.CurrentHull != null ? targetHull.AiTarget : wall.AiTarget; - if (wall.IsHorizontal) - { - targetWorldPos.Y = targetHull.WorldRect.Y - targetHull.Rect.Height / 2; - } - else - { - targetWorldPos.X = targetHull.WorldRect.Center.X; - } - steeringManager.SteeringManual(deltaTime, Vector2.Normalize(targetWorldPos - Character.WorldPosition)); - SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15); - return true; - } - return false; - } - private readonly List attackLimbs = new List(); private readonly List weights = new List(); private Limb GetAttackLimb(Vector2 attackWorldPos, Limb ignoredLimb = null) @@ -1471,106 +1504,6 @@ namespace Barotrauma } } - private void UpdateWallTarget() - { - wallTarget = null; - if (SelectedAiTarget == null) { return; } - if (SelectedAiTarget.Entity == null) { return; } - //check if there's a wall between the target and the Character - Vector2 rayStart = SimPosition; - Vector2 rayEnd = SelectedAiTarget.SimPosition; - if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null) - { - 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 && (wall.Submarine.Info.IsPlayer || wall.Submarine.Info.IsOutpost && TargetOutposts)) - { - int sectionIndex = wall.FindSectionIndex(ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition)); - 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)) - { - sectionIndex = i; - break; - } - else - { - //otherwise 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); - } - } - } - } - if (!Character.AnimController.CanEnterSubmarine && wallTarget == null) - { - if (closestBody.UserData is Structure w && w.Submarine != null || closestBody.UserData is Item i && i.Submarine != null) - { - // Cannot reach the target, because it's blocked by a disabled wall or a door - State = AIState.Idle; - IgnoreTarget(SelectedAiTarget); - ResetAITarget(); - } - } - } - } - - private bool IsWallDisabled(Structure wall) - { - bool isDisabled = true; - for (int i = 0; i < wall.Sections.Length; i++) - { - if (!wall.SectionBodyDisabled(i)) - { - isDisabled = false; - break; - } - } - return isDisabled; - } - public override void OnAttacked(Character attacker, AttackResult attackResult) { float reactionTime = Rand.Range(0.1f, 0.3f); @@ -1603,7 +1536,7 @@ namespace Barotrauma if (!isFriendly && attackResult.Damage > 0.0f) { bool canAttack = attacker.Submarine == Character.Submarine && canAttackCharacters || attacker.Submarine != null && canAttackWalls; - if (Character.Params.AI.AttackWhenProvoked && canAttack) + if (AIParams.AttackWhenProvoked && canAttack) { if (attacker.IsHusk) { @@ -1661,7 +1594,7 @@ namespace Barotrauma // Only allow to react once. Otherwise would attack the target with only a fraction of a cooldown bool retaliate = !isFriendly && SelectedAiTarget != attacker.AiTarget && attacker.Submarine == Character.Submarine; - bool avoidGunFire = Character.Params.AI.AvoidGunfire && attacker.Submarine != Character.Submarine; + bool avoidGunFire = AIParams.AvoidGunfire && attacker.Submarine != Character.Submarine; if (State == AIState.Attack && !IsCoolDownRunning) { @@ -1686,7 +1619,7 @@ namespace Barotrauma avoidTimer = AIParams.AvoidTime * Rand.Range(0.75f, 1.25f); SelectTarget(attacker.AiTarget); } - if (Character.HealthPercentage <= FleeHealthThreshold) + if (Math.Max(Character.HealthPercentage, 0) < FleeHealthThreshold) { State = AIState.Flee; avoidTimer = AIParams.MinFleeTime * Rand.Range(0.75f, 1.25f); @@ -1707,6 +1640,7 @@ namespace Barotrauma if (aiTarget != null && SelectedAiTarget != aiTarget) { SelectTarget(aiTarget, GetTargetMemory(SelectedAiTarget, true).Priority); + State = AIState.Attack; } } IDamageable damageTarget = wallTarget != null ? wallTarget.Structure : SelectedAiTarget.Entity as IDamageable; @@ -1974,7 +1908,7 @@ namespace Barotrauma bool targetingFromOutsideToInside = item.CurrentHull != null && character.CurrentHull == null; if (targetingFromOutsideToInside) { - if (door != null && !canAttackDoors || !canAttackWalls) + if (door != null && (!canAttackDoors && !AIParams.Infiltrate) || !canAttackWalls) { // Can't reach continue; @@ -2122,16 +2056,14 @@ namespace Barotrauma bool isOutdoor = door.LinkedGap?.FlowTargetHull != null && !door.LinkedGap.IsRoomToRoom; // Ignore inner doors when outside if (character.CurrentHull == null && !isOutdoor) { continue; } - bool isOpen = door.IsOpen || door.IsBroken; - if (!isOpen && !canAttackDoors || (isOutdoor && !AIParams.TargetOuterWalls)) + bool isOpen = door.CanBeTraversed; + if (!isOpen) { - // Ignore doors that are not open if cannot attack doors or shouldn't target outer doors. - continue; + if (!canAttackDoors || isOutdoor && !AIParams.TargetOuterWalls) { continue; } } - if (isOpen && (!Character.AnimController.CanEnterSubmarine || !AggressiveBoarding)) + else if (!Character.AnimController.CanEnterSubmarine) { - // Ignore broken and open doors - // Aggressive boarders don't ignore open doors, because they use them for getting in. + // Ignore broken and open doors, if cannot enter submarine continue; } if (AggressiveBoarding) @@ -2157,8 +2089,9 @@ namespace Barotrauma if (targetingTag == null) { continue; } var targetParams = GetTargetParams(targetingTag); if (targetParams == null) { continue; } - if (targetParams.IgnoreWhileInside && character.CurrentHull != null) { continue; } - if (targetParams.IgnoreWhileOutside && character.CurrentHull == null) { continue; } + if (targetParams.IgnoreInside && character.CurrentHull != null) { continue; } + if (targetParams.IgnoreOutside && character.CurrentHull == null) { continue; } + if (targetParams.IgnoreIncapacitated && targetCharacter != null && targetCharacter.IsIncapacitated) { continue; } if (targetParams.State == AIState.Observe || targetParams.State == AIState.Eat) { if (targetCharacter != null && targetCharacter.Submarine != Character.Submarine) @@ -2270,7 +2203,7 @@ namespace Barotrauma foreach (var gap in Character.CurrentHull.ConnectedGaps) { var door = gap.ConnectedDoor; - if (door == null || !door.IsOpen && !door.IsBroken) + if (door == null) { var wall = gap.ConnectedWall; if (wall != null) @@ -2293,7 +2226,7 @@ namespace Barotrauma newTarget = aiTarget; selectedTargetMemory = targetMemory; targetValue = valueModifier; - targetingParams = GetTargetParams(targetingTag); + targetingParams = targetParams; } } @@ -2332,6 +2265,149 @@ namespace Barotrauma return SelectedAiTarget; } + class WallTarget + { + public Vector2 Position; + public Structure Structure; + public int SectionIndex; + + public WallTarget(Vector2 position, Structure structure = null, int sectionIndex = -1) + { + Position = position; + Structure = structure; + SectionIndex = sectionIndex; + } + } + + private WallTarget wallTarget; + + private void UpdateWallTarget(int requiredHoleCount) + { + wallTarget = null; + if (State == AIState.Flee || State == AIState.Escape) { return; } + if (AIParams.Infiltrate && 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) + { + 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)) + { + int sectionIndex = wall.FindSectionIndex(ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition)); + 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; + } + } + 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); + } + } + } + } + if (!Character.AnimController.CanEnterSubmarine && wallTarget == null) + { + if (closestBody.UserData is Structure w && w.Submarine != null || closestBody.UserData is Item i && i.Submarine != null) + { + // Cannot reach the target, because it's blocked by a disabled wall or a door + State = AIState.Idle; + IgnoreTarget(SelectedAiTarget); + ResetAITarget(); + } + } + } + } + + private bool TrySteerThroughGaps(float deltaTime) + { + 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); + return section?.gap != null && SteerThroughGap(wallTarget.Structure, section, targetPos, deltaTime); + } + else if (SelectedAiTarget != null) + { + if (SelectedAiTarget.Entity is Structure wall) + { + for (int i = 0; i < wall.Sections.Length; i++) + { + WallSection section = wall.Sections[i]; + if (CanPassThroughHole(wall, i, requiredHoleCount) && section?.gap != null) + { + return SteerThroughGap(wall, section, wall.SectionPosition(i, true), deltaTime); + } + } + } + else if (SelectedAiTarget.Entity is Item i) + { + var door = i.GetComponent(); + // Don't try to enter dry hulls if cannot walk or if the gap is too narrow + if (door?.LinkedGap?.FlowTargetHull != null && !door.LinkedGap.IsRoomToRoom && door.CanBeTraversed) + { + if (Character.AnimController.CanWalk || door.LinkedGap.FlowTargetHull.WaterPercentage > 25) + { + if (door.LinkedGap.Size > ConvertUnits.ToDisplayUnits(colliderWidth)) + { + return SteerThroughGap(door.LinkedGap, door.LinkedGap.FlowTargetHull.WorldPosition, deltaTime, maxDistance: 100); + } + } + } + } + } + return false; + } + private AITargetMemory GetTargetMemory(AITarget target, bool addIfNotFound) { if (!targetMemories.TryGetValue(target, out AITargetMemory memory)) @@ -2631,28 +2707,38 @@ namespace Barotrauma } } - public bool CanPassThroughHole(Structure wall, int sectionIndex) + public override bool SteerThroughGap(Structure wall, WallSection section, Vector2 targetWorldPos, float deltaTime) { - if (!wall.SectionBodyDisabled(sectionIndex)) return false; - int holeCount = 1; - for (int j = sectionIndex - 1; j > sectionIndex - requiredHoleCount; j--) + wallTarget = null; + LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 2); + Character.AnimController.ReleaseStuckLimbs(); + bool success = base.SteerThroughGap(wall, section, targetWorldPos, deltaTime); + if (success) { - if (wall.SectionBodyDisabled(j)) - holeCount++; - else - break; + // If already inside, target the hull, else target the wall. + SelectedAiTarget = Character.CurrentHull != null ? section.gap.AiTarget : wall.AiTarget; + SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 1); } - for (int j = sectionIndex + 1; j < sectionIndex + requiredHoleCount; j++) - { - if (wall.SectionBodyDisabled(j)) - holeCount++; - else - break; - } - - return holeCount >= requiredHoleCount; + IsSteeringThroughGap = success; + return success; } + public override bool SteerThroughGap(Gap gap, Vector2 targetWorldPos, float deltaTime, float maxDistance = -1) + { + wallTarget = null; + LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 2); + Character.AnimController.ReleaseStuckLimbs(); + bool success = base.SteerThroughGap(gap, targetWorldPos, deltaTime, maxDistance); + if (success) + { + SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 1); + } + IsSteeringThroughGap = success; + return success; + } + + public bool CanPassThroughHole(Structure wall, int sectionIndex) => CanPassThroughHole(wall, sectionIndex, requiredHoleCount); + private readonly List targetLimbs = new List(); public Limb GetTargetLimb(Limb attackLimb, Character target, LimbType targetLimbType = LimbType.None) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 236e2ad6f..bea255d4a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -29,7 +29,9 @@ namespace Barotrauma private float flipTimer; private const float FlipInterval = 0.5f; - public static float HULL_SAFETY_THRESHOLD = 50; + public const float HULL_SAFETY_THRESHOLD = 40; + public const float HULL_LOW_OXYGEN_PERCENTAGE = 30; + private static readonly float characterWaitOnSwitch = 5; public readonly HashSet UnreachableHulls = new HashSet(); @@ -60,10 +62,7 @@ namespace Barotrauma public IndoorsSteeringManager PathSteering => insideSteering as IndoorsSteeringManager; public HumanoidAnimController AnimController => Character.AnimController as HumanoidAnimController; - public override AIObjectiveManager ObjectiveManager - { - get { return objectiveManager; } - } + public AIObjectiveManager ObjectiveManager => objectiveManager; public Order CurrentOrder { @@ -79,9 +78,7 @@ namespace Barotrauma public float CurrentHullSafety { get; private set; } = 100; - private readonly Dictionary damageDoneByAttacker = new Dictionary(); - private readonly HashSet attackers = new HashSet(); - + private readonly Dictionary structureDamageAccumulator = new Dictionary(); private readonly Dictionary knownHulls = new Dictionary(); private class HullSafety { @@ -132,22 +129,6 @@ namespace Barotrauma { if (DisableCrewAI || Character.Removed) { return; } - //slowly forget about damage done by attackers - foreach (Character enemy in attackers) - { - float cumulativeDamage = damageDoneByAttacker[enemy]; - if (cumulativeDamage > 0) - { - float reduction = deltaTime; - if (cumulativeDamage < 2) - { - // If the damage is very low, let's not forget so quickly, or we can't cumulate the damage from repair tools (high frequency, low damage) - reduction *= 0.5f; - } - damageDoneByAttacker[enemy] -= reduction; - } - } - bool isIncapacitated = Character.IsIncapacitated; if (freezeAI && !isIncapacitated) { @@ -188,7 +169,7 @@ namespace Barotrauma } bool IsCloseEnoughToTargetSub(float threshold) => SelectedAiTarget?.Entity?.Submarine is Submarine sub && sub != null && Vector2.DistanceSquared(Character.WorldPosition, sub.WorldPosition) < MathUtils.Pow(Math.Max(sub.Borders.Size.X, sub.Borders.Size.Y) / 2 + threshold, 2); - bool hasValidPath = steeringManager is IndoorsSteeringManager pathSteering && pathSteering.CurrentPath != null && !pathSteering.CurrentPath.Finished && !pathSteering.CurrentPath.Unreachable; + bool hasValidPath = HasValidPath(); if (Character.Submarine == null && hasValidPath) { @@ -259,7 +240,7 @@ namespace Barotrauma { if (Character.CurrentHull != null) { - if (Character.TeamID == Character.TeamType.FriendlyNPC) + if (Character.TeamID == CharacterTeamType.FriendlyNPC) { // Outpost npcs don't inform each other about threads, like crew members do. VisibleHulls.ForEach(h => RefreshHullSafety(h)); @@ -386,10 +367,11 @@ namespace Barotrauma if (isCarrying) { - if (findItemState != FindItemState.OtherItem) + if (findItemState == FindItemState.DivingSuit && ObjectiveManager.IsCurrentObjective()) { if (ObjectiveManager.GetActiveObjective() is AIObjectiveGoTo gotoObjective && NeedsDivingGearOnPath(gotoObjective)) { + // Don't try to put the diving suit in a locker if the suit would be needed in any hull in the path to the locker. gotoObjective.Abandon = true; } } @@ -414,7 +396,7 @@ namespace Barotrauma { shouldKeepTheGearOn = false; } - else if (Character.CurrentHull.Oxygen < CharacterHealth.LowOxygenThreshold) + else if (Character.CurrentHull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 10) { shouldKeepTheGearOn = true; } @@ -558,39 +540,37 @@ namespace Barotrauma // Other items if (isCarrying) { return; } if (!ObjectiveManager.CurrentObjective.AllowAutomaticItemUnequipping || !ObjectiveManager.GetActiveObjective().AllowAutomaticItemUnequipping) { return; } - foreach (var item in Character.Inventory.Items) + + if (findItemState == FindItemState.None || findItemState == FindItemState.OtherItem) { - if (item == null) { continue; } - if (Character.HasEquippedItem(item) && - (Character.Inventory.IsInLimbSlot(item, InvSlotType.RightHand) || - Character.Inventory.IsInLimbSlot(item, InvSlotType.LeftHand) || - Character.Inventory.IsInLimbSlot(item, InvSlotType.RightHand | InvSlotType.LeftHand))) + for (int i = 0; i < 2; i++) { + var hand = i == 0 ? InvSlotType.RightHand : InvSlotType.LeftHand; + Item item = Character.Inventory.GetItemInLimbSlot(hand); + if (item == null) { continue; } + if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, new List() { InvSlotType.Any })) { - if (findItemState == FindItemState.None || findItemState == FindItemState.OtherItem) + findItemState = FindItemState.OtherItem; + if (FindSuitableContainer(item, out Item targetContainer)) { - findItemState = FindItemState.OtherItem; - if (FindSuitableContainer(item, out Item targetContainer)) + findItemState = FindItemState.None; + itemIndex = 0; + if (targetContainer != null) { - findItemState = FindItemState.None; - itemIndex = 0; - if (targetContainer != null) + var decontainObjective = new AIObjectiveDecontainItem(Character, item, ObjectiveManager, targetContainer: targetContainer.GetComponent()); + decontainObjective.Abandoned += () => { - var decontainObjective = new AIObjectiveDecontainItem(Character, item, ObjectiveManager, targetContainer: targetContainer.GetComponent()); - decontainObjective.Abandoned += () => - { - ReequipUnequipped(); - IgnoredItems.Add(targetContainer); - }; - ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true); - return; - } - else - { - item.Drop(Character); - HandleRelocation(item); - } + ReequipUnequipped(); + IgnoredItems.Add(targetContainer); + }; + ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true); + return; + } + else + { + item.Drop(Character); + HandleRelocation(item); } } } @@ -602,7 +582,7 @@ namespace Barotrauma private void HandleRelocation(Item item) { - if (item.Submarine?.TeamID == Character.TeamType.FriendlyNPC) + if (item.Submarine?.TeamID == CharacterTeamType.FriendlyNPC) { if (itemsToRelocate.Contains(item)) { return; } itemsToRelocate.Add(item); @@ -627,7 +607,7 @@ namespace Barotrauma { if (item.ParentInventory.Owner is Character c) { - if (c.TeamID == Character.TeamType.Team1 || c.TeamID == Character.TeamType.Team2) + if (c.TeamID == CharacterTeamType.Team1 || c.TeamID == CharacterTeamType.Team2) { // Taken by a player/bot (if npc or monster would take the item, we'd probably still want it to spawn back to the main sub. return; @@ -654,18 +634,6 @@ namespace Barotrauma } } - public void ReequipUnequipped() - { - foreach (var item in unequippedItems) - { - if (item != null && !item.Removed && Character.HasItem(item)) - { - TakeItem(item, Character.Inventory, equip: true, dropOtherIfCannotMove: true, allowSwapping: true, storeUnequipped: false); - } - } - unequippedItems.Clear(); - } - private enum FindItemState { None, @@ -681,23 +649,23 @@ namespace Barotrauma public static bool FindSuitableContainer(Character character, Item containableItem, List ignoredItems, ref int itemIndex, out Item suitableContainer) { suitableContainer = null; - if (character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: ignoredItems, customPriorityFunction: i => + if (character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: ignoredItems, positionalReference: containableItem, customPriorityFunction: i => { if (i.IsThisOrAnyContainerIgnoredByAI()) { return 0; } var container = i.GetComponent(); if (container == null) { return 0; } - if (container.Inventory.IsFull()) { return 0; } + if (!container.Inventory.CanBePut(containableItem)) { return 0; } if (container.ShouldBeContained(containableItem, out bool isRestrictionsDefined)) { if (isRestrictionsDefined) { - return 4; + return 10; } else { - if (containableItem.Prefab.IsContainerPreferred(container, out bool isPreferencesDefined, out bool isSecondary)) + if (containableItem.IsContainerPreferred(container, out bool isPreferencesDefined, out bool isSecondary)) { - return isPreferencesDefined ? isSecondary ? 2 : 3 : 1; + return isPreferencesDefined ? isSecondary ? 2 : 5 : 1; } else { @@ -749,6 +717,18 @@ namespace Barotrauma targetHull = hull; } } + foreach (var ballastFlora in MapCreatures.Behavior.BallastFloraBehavior.EntityList) + { + if (ballastFlora.Parent?.Submarine != Character.Submarine) { continue; } + if (!ballastFlora.HasBrokenThrough) { continue; } + // Don't react to the first two branches, because they are usually in the very edges of the room. + if (ballastFlora.Branches.Count(b => !b.Removed && b.Health > 0 && b.CurrentHull == hull) > 2) + { + var orderPrefab = Order.GetPrefab("reportballastflora"); + newOrder = new Order(orderPrefab, hull, null, orderGiver: Character); + targetHull = hull; + } + } if (!isFighting) { foreach (var gap in hull.ConnectedGaps) @@ -798,7 +778,7 @@ namespace Barotrauma } if (newOrder != null) { - if (Character.TeamID == Character.TeamType.FriendlyNPC) + if (Character.TeamID == CharacterTeamType.FriendlyNPC) { Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName, givingOrderToSelf: false), ChatMessageType.Default, identifier: newOrder.Prefab.Identifier + (targetHull?.DisplayName ?? "null"), @@ -837,8 +817,8 @@ namespace Barotrauma Character.Speak(TextManager.Get("DialogBleeding"), null, Rand.Range(0.5f, 5.0f), "bleeding", 30.0f); } - if (Character.PressureTimer > 50.0f && Character.CurrentHull != null) - { + if (Character.PressureTimer > 50.0f && Character.CurrentHull?.DisplayName != null) + { Character.Speak(TextManager.GetWithVariable("DialogPressure", "[roomname]", Character.CurrentHull.DisplayName, true), null, Rand.Range(0.5f, 5.0f), "pressure", 30.0f); } } @@ -895,15 +875,6 @@ namespace Barotrauma if (totalDamage <= 0.01f) { return; } if (Character.IsBot) { - if (attacker != null) - { - if (!damageDoneByAttacker.ContainsKey(attacker)) - { - damageDoneByAttacker[attacker] = 0.0f; - } - damageDoneByAttacker[attacker] += totalDamage; - attackers.Add(attacker); - } if (!freezeAI && !Character.IsDead && Character.IsIncapacitated) { // Removes the combat objective and resets all objectives. @@ -953,7 +924,7 @@ namespace Barotrauma foreach (Character otherCharacter in Character.CharacterList) { if (otherCharacter == Character || otherCharacter.IsDead || otherCharacter.IsUnconscious || otherCharacter.Removed || - otherCharacter.Info?.Job == null || otherCharacter.TeamID != Character.TeamType.FriendlyNPC || + otherCharacter.Info?.Job == null || otherCharacter.TeamID != CharacterTeamType.FriendlyNPC || !(otherCharacter.AIController is HumanAIController otherHumanAI) || otherCharacter.IsInstigator) { @@ -1045,7 +1016,7 @@ namespace Barotrauma // The guards don't react when the player attacks instigators. return c.IsSecurity ? AIObjectiveCombat.CombatMode.None : (Character.CombatAction != null ? Character.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat); } - else if (attacker.TeamID == Character.TeamType.FriendlyNPC) + else if (attacker.TeamID == CharacterTeamType.FriendlyNPC) { if (c.IsSecurity) { @@ -1078,7 +1049,7 @@ namespace Barotrauma } } - private void AddCombatObjective(AIObjectiveCombat.CombatMode mode, Character attacker, float delay = 0, Func abortCondition = null, Action onAbort = null, bool allowHoldFire = false) + private void AddCombatObjective(AIObjectiveCombat.CombatMode mode, Character attacker, float delay = 0, Func abortCondition = null, Action onAbort = null, Action onCompleted = null, bool allowHoldFire = false) { if (mode == AIObjectiveCombat.CombatMode.None) { return; } if (Character.IsDead || Character.IsIncapacitated) { return; } @@ -1117,49 +1088,19 @@ namespace Barotrauma { objective.Abandoned += onAbort; } + if (onCompleted != null) + { + objective.Completed += onCompleted; + } return objective; } } + public void SetOrder(Order order, string option, Character orderGiver, bool speak = true) { CurrentOrderOption = option; CurrentOrder = order; - objectiveManager.SetOrder(order, option, orderGiver); - if (ObjectiveManager.CurrentOrder != null && speak && Character.SpeechImpediment < 100.0f) - { - if (ObjectiveManager.CurrentOrder is AIObjectiveRepairItems repairItems && repairItems.Targets.None()) - { - Character.Speak(TextManager.Get("DialogNoRepairTargets"), null, 3.0f, "norepairtargets"); - } - else if (ObjectiveManager.CurrentOrder is AIObjectiveChargeBatteries chargeBatteries && chargeBatteries.Targets.None()) - { - Character.Speak(TextManager.Get("DialogNoBatteries"), null, 3.0f, "nobatteries"); - } - else if (ObjectiveManager.CurrentOrder is AIObjectiveExtinguishFires extinguishFires && extinguishFires.Targets.None()) - { - Character.Speak(TextManager.Get("DialogNoFire"), null, 3.0f, "nofire"); - } - else if (ObjectiveManager.CurrentOrder is AIObjectiveFixLeaks fixLeaks && fixLeaks.Targets.None()) - { - Character.Speak(TextManager.Get("DialogNoLeaks"), null, 3.0f, "noleaks"); - } - else if (ObjectiveManager.CurrentOrder is AIObjectiveFightIntruders fightIntruders && fightIntruders.Targets.None()) - { - Character.Speak(TextManager.Get("DialogNoEnemies"), null, 3.0f, "noenemies"); - } - else if (ObjectiveManager.CurrentOrder is AIObjectiveRescueAll rescueAll && rescueAll.Targets.None()) - { - Character.Speak(TextManager.Get("DialogNoRescueTargets"), null, 3.0f, "norescuetargets"); - } - else if (ObjectiveManager.CurrentOrder is AIObjectivePumpWater pumpWater && pumpWater.Targets.None()) - { - Character.Speak(TextManager.Get("DialogNoPumps"), null, 3.0f, "nopumps"); - } - else - { - Character.Speak(TextManager.Get("DialogAffirmative"), null, 1.0f); - } - } + objectiveManager.SetOrder(order, option, orderGiver, speak); } public override void SelectTarget(AITarget target) @@ -1213,56 +1154,6 @@ namespace Barotrauma return true; } - private readonly HashSet unequippedItems = new HashSet(); - public bool TakeItem(Item item, Inventory targetInventory, bool equip, bool dropOtherIfCannotMove = true, bool allowSwapping = false, bool storeUnequipped = false) - { - var pickable = item.GetComponent(); - if (item.ParentInventory is ItemInventory itemInventory) - { - if (!itemInventory.Container.HasRequiredItems(Character, addMessage: false)) { return false; } - } - if (equip) - { - int targetSlot = -1; - //check if all the slots required by the item are free - foreach (InvSlotType slots in pickable.AllowedSlots) - { - if (slots.HasFlag(InvSlotType.Any)) { continue; } - for (int i = 0; i < targetInventory.Items.Length; i++) - { - if (targetInventory is CharacterInventory characterInventory) - { - //slot not needed by the item, continue - if (!slots.HasFlag(characterInventory.SlotTypes[i])) { continue; } - } - targetSlot = i; - //slot free, continue - var otherItem = targetInventory.Items[i]; - if (otherItem == null) { continue; } - //try to move the existing item to LimbSlot.Any and continue if successful - if (otherItem.AllowedSlots.Contains(InvSlotType.Any) && targetInventory.TryPutItem(otherItem, Character, CharacterInventory.anySlot)) - { - if (storeUnequipped && targetInventory.Owner == Character) - { - unequippedItems.Add(otherItem); - } - continue; - } - if (dropOtherIfCannotMove) - { - //if everything else fails, simply drop the existing item - otherItem.Drop(Character); - } - } - } - return targetInventory.TryPutItem(item, targetSlot, allowSwapping, allowCombine: false, Character); - } - else - { - return targetInventory.TryPutItem(item, Character, CharacterInventory.anySlot); - } - } - public static bool NeedsDivingGear(Hull hull, out bool needsSuit) { needsSuit = false; @@ -1274,7 +1165,7 @@ namespace Barotrauma needsSuit = true; return true; } - if (hull.WaterPercentage > 60 || hull.Oxygen < CharacterHealth.LowOxygenThreshold) + if (hull.WaterPercentage > 60 || hull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 1) { return true; } @@ -1294,20 +1185,118 @@ namespace Barotrauma public static bool HasDivingMask(Character character, float conditionPercentage = 0) => HasItem(character, AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR, out _, AIObjectiveFindDivingGear.OXYGEN_SOURCE, conditionPercentage, requireEquipped: true); private static List matchingItems = new List(); - public static bool HasItem(Character character, string tagOrIdentifier, out IEnumerable items, string containedTag = null, float conditionPercentage = 0, bool requireEquipped = false) + + /// + /// Note: uses a single list for matching items. The item is reused each time when the method is called. So if you use the method twice, and then refer to the first items, you'll actually get the second. + /// To solve this, create a copy of the collection or change the code so that you first handle the first items and only after that query for the next items. + /// + public static bool HasItem(Character character, string tagOrIdentifier, out IEnumerable items, string containedTag = null, float conditionPercentage = 0, bool requireEquipped = false, bool recursive = true, Func predicate = null) { matchingItems.Clear(); items = matchingItems; if (character == null) { return false; } if (character.Inventory == null) { return false; } - matchingItems = character.Inventory.FindAllItems(i => i.Prefab.Identifier == tagOrIdentifier || i.HasTag(tagOrIdentifier), recursive: true, matchingItems); - items = matchingItems; - return matchingItems.Any(i => i != null && + matchingItems = character.Inventory.FindAllItems(i => (i.Prefab.Identifier == tagOrIdentifier || i.HasTag(tagOrIdentifier)) && i.ConditionPercentage >= conditionPercentage && (!requireEquipped || character.HasEquippedItem(i)) && - (containedTag == null || - (i.OwnInventory?.Items != null && - i.OwnInventory.Items.Any(it => it != null && it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage)))); + (predicate == null || predicate(i)), recursive, matchingItems); + items = matchingItems; + return matchingItems.Any(i => i != null && (containedTag == null || i.ContainedItems.Any(it => it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage))); + } + + public static void StructureDamaged(Structure structure, float damageAmount, Character character) + { + const float MaxDamagePerSecond = 5.0f; + const float MaxDamagePerFrame = MaxDamagePerSecond * (float)Timing.Step; + + const float WarningThreshold = 5.0f; + const float ArrestThreshold = 20.0f; + const float KillThreshold = 50.0f; + + if (character == null || damageAmount <= 0.0f) { return; } + if (structure?.Submarine == null || !structure.Submarine.Info.IsOutpost || character.TeamID == structure.Submarine.TeamID) { return; } + //structure not indestructible = something that's "meant" to be destroyed, like an ice wall in mines + if (!structure.Prefab.IndestructibleInOutposts) { return; } + + bool someoneSpoke = false; + float maxAccumulatedDamage = 0.0f; + foreach (Character otherCharacter in Character.CharacterList) + { + if (otherCharacter == character || otherCharacter.TeamID == character.TeamID || otherCharacter.IsDead || + otherCharacter.Info?.Job == null || + !(otherCharacter.AIController is HumanAIController otherHumanAI) || + !otherHumanAI.VisibleHulls.Contains(character.CurrentHull)) + { + continue; + } + if (!otherCharacter.CanSeeCharacter(character)) { continue; } + + if (!otherHumanAI.structureDamageAccumulator.ContainsKey(character)) { otherHumanAI.structureDamageAccumulator.Add(character, 0.0f); } + float prevAccumulatedDamage = otherHumanAI.structureDamageAccumulator[character]; + otherHumanAI.structureDamageAccumulator[character] += MathHelper.Clamp(damageAmount, -MaxDamagePerFrame, MaxDamagePerFrame); + float accumulatedDamage = Math.Max(otherHumanAI.structureDamageAccumulator[character], maxAccumulatedDamage); + maxAccumulatedDamage = Math.Max(accumulatedDamage, maxAccumulatedDamage); + + if (GameMain.GameSession?.Campaign?.Map?.CurrentLocation != null) + { + var reputationLoss = damageAmount * Reputation.ReputationLossPerWallDamage; + GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.Value -= reputationLoss; + } + + if (accumulatedDamage <= WarningThreshold) { return; } + + if (accumulatedDamage > WarningThreshold && prevAccumulatedDamage <= WarningThreshold && + !someoneSpoke && !character.IsIncapacitated && character.Stun <= 0.0f) + { + //if the damage is still fairly low, wait and see if the character keeps damaging the walls to the point where we need to intervene + if (accumulatedDamage < ArrestThreshold) + { + if (otherHumanAI.ObjectiveManager.IsCurrentObjective()) + { + (otherHumanAI.ObjectiveManager.CurrentObjective as AIObjectiveIdle)?.FaceTargetAndWait(character, 5.0f); + } + } + otherCharacter.Speak(TextManager.Get("dialogdamagewallswarning"), null, Rand.Range(0.5f, 1.0f), "damageoutpostwalls", 10.0f); + someoneSpoke = true; + } + // React if we are security + if ((accumulatedDamage > ArrestThreshold && prevAccumulatedDamage <= ArrestThreshold) || + (accumulatedDamage > KillThreshold && prevAccumulatedDamage <= KillThreshold)) + { + var combatMode = accumulatedDamage > KillThreshold ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Arrest; + if (!TriggerSecurity(otherHumanAI, combatMode)) + { + // Else call the others + foreach (Character security in Character.CharacterList.Where(c => c.TeamID == otherCharacter.TeamID).OrderByDescending(c => Vector2.DistanceSquared(character.WorldPosition, c.WorldPosition))) + { + if (!TriggerSecurity(security.AIController as HumanAIController, combatMode)) + { + // Only alert one guard at a time + return; + } + } + } + } + } + + bool TriggerSecurity(HumanAIController humanAI, AIObjectiveCombat.CombatMode combatMode) + { + if (humanAI == null) { return false; } + if (!humanAI.Character.IsSecurity) { return false; } + if (humanAI.ObjectiveManager.IsCurrentObjective()) { return false; } + humanAI.AddCombatObjective(combatMode, character, delay: GetReactionTime(), allowHoldFire: true, onCompleted: () => + { + //if the target is arrested successfully, reset the damage accumulator + foreach (Character anyCharacter in Character.CharacterList) + { + if (anyCharacter.AIController is HumanAIController anyAI) + { + anyAI.structureDamageAccumulator?.Remove(character); + } + } + }); + return true; + } } public static void ItemTaken(Item item, Character character) @@ -1316,7 +1305,7 @@ namespace Barotrauma Character thief = character; bool someoneSpoke = false; - if (item.SpawnedInOutpost && thief.TeamID != Character.TeamType.FriendlyNPC && !item.HasTag("handlocker")) + if (item.SpawnedInOutpost && thief.TeamID != CharacterTeamType.FriendlyNPC && !item.HasTag("handlocker")) { foreach (Character otherCharacter in Character.CharacterList) { @@ -1495,11 +1484,13 @@ namespace Barotrauma humanAI.ObjectiveManager.GetObjective()?.ReportedTargets.Remove(target)); } - public float GetDamageDoneByAttacker(Character attacker) + public float GetDamageDoneByAttacker(Character otherCharacter) { - if (!damageDoneByAttacker.TryGetValue(attacker, out float dmg)) + float dmg = 0; + Character.Attacker attacker = Character.LastAttackers.LastOrDefault(a => a.Character == otherCharacter); + if (attacker != null) { - dmg = 0; + dmg = attacker.Damage; } return dmg; } @@ -1550,9 +1541,10 @@ namespace Barotrauma { if (hull == null) { return 0; } if (hull.LethalPressure > 0 && character.PressureProtection <= 0) { return 0; } - // TODO: take the visiblehulls into account? - float oxygenFactor = ignoreOxygen ? 1 : MathHelper.Lerp(0.25f, 1, hull.OxygenPercentage / 100); - float waterFactor = ignoreWater ? 1 : MathHelper.Lerp(1, 0.25f, hull.WaterPercentage / 100); + // Oxygen factor should be 1 with 70% oxygen or more and 0.1 when the oxygen level is 30% or lower. + // With insufficient oxygen, the safety of the hull should be 39, all the other factors aside. So, just below the HULL_SAFETY_THRESHOLD. + float oxygenFactor = ignoreOxygen ? 1 : MathHelper.Lerp((HULL_SAFETY_THRESHOLD - 1) / 100, 1, MathUtils.InverseLerp(HULL_LOW_OXYGEN_PERCENTAGE, 100 - HULL_LOW_OXYGEN_PERCENTAGE, hull.OxygenPercentage)); + float waterFactor = ignoreWater ? 1 : MathHelper.Lerp(1, HULL_SAFETY_THRESHOLD / 2 / 100, hull.WaterPercentage / 100); if (!character.NeedsAir) { oxygenFactor = 1; @@ -1637,7 +1629,7 @@ namespace Barotrauma if (!teamGood) { return false; } bool speciesGood = other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group); if (!speciesGood) { return false; } - if (me.TeamID == Character.TeamType.FriendlyNPC && other.TeamID == Character.TeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign) + if (me.TeamID == CharacterTeamType.FriendlyNPC && other.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign) { var reputation = campaign.Map?.CurrentLocation?.Reputation; if (reputation != null && reputation.NormalizedValue < Reputation.HostileThreshold) @@ -1651,7 +1643,7 @@ namespace Barotrauma private static bool IsOnFriendlyTeam(GameMode mode, Character me, Character other) { // Only enemies are in the Team "None" - bool friendlyTeam = me.TeamID != Character.TeamType.None && other.TeamID != Character.TeamType.None; + bool friendlyTeam = me.TeamID != CharacterTeamType.None && other.TeamID != CharacterTeamType.None; // When playing a combat mission, we need to be on the same team to be friendlies if (friendlyTeam && mode is MissionMode mm && mm.Mission is CombatMission) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs index 12b112cd7..022f6b756 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs @@ -50,9 +50,9 @@ namespace Barotrauma /// Returns true if the current or the next node is in ladders. /// public bool InLadders => - currentPath != null && - currentPath.CurrentNode != null && (currentPath.CurrentNode.Ladders != null && !currentPath.CurrentNode.Ladders.Item.NonInteractable || - (currentPath.NextNode != null && currentPath.NextNode.Ladders != null && !currentPath.NextNode.Ladders.Item.NonInteractable)); + currentPath != null && currentPath.CurrentNode != null && + (currentPath.CurrentNode.Ladders != null && currentPath.CurrentNode.Ladders.Item.IsInteractable(character) || + (currentPath.NextNode != null && currentPath.NextNode.Ladders != null && currentPath.NextNode.Ladders.Item.IsInteractable(character))); /// /// Returns true if any node in the path is in stairs @@ -70,7 +70,7 @@ namespace Barotrauma if (currentPath.NextNode == null) { return false; } var currentLadder = currentPath.CurrentNode.Ladders; if (currentLadder == null) { return false; } - if (currentLadder.Item.NonInteractable) { return false; } + if (!currentLadder.Item.IsInteractable(character)) { return false; } var nextLadder = GetNextLadder(); return nextLadder != null && nextLadder == currentLadder; } @@ -123,7 +123,7 @@ namespace Barotrauma { if (currentPath == null) { return null; } if (currentPath.NextNode == null) { return null; } - if (currentPath.NextNode.Ladders != null && !currentPath.NextNode.Ladders.Item.NonInteractable) + if (currentPath.NextNode.Ladders != null && currentPath.NextNode.Ladders.Item.IsInteractable(character)) { return currentPath.NextNode.Ladders; } @@ -134,7 +134,7 @@ namespace Barotrauma { var node = currentPath.Nodes[index]; if (node == null) { return null; } - if (node.Ladders != null && !node.Ladders.Item.NonInteractable) + if (node.Ladders != null && node.Ladders.Item.IsInteractable(character)) { return node.Ladders; } @@ -146,7 +146,7 @@ namespace Barotrauma { node = currentPath.Nodes[index]; if (node == null) { return null; } - if (node.Ladders != null && !node.Ladders.Item.NonInteractable) + if (node.Ladders != null && node.Ladders.Item.IsInteractable(character)) { return node.Ladders; } @@ -295,7 +295,7 @@ namespace Barotrauma // Only humanoids can climb ladders bool canClimb = character.AnimController is HumanoidAnimController && !character.LockHands; Ladder currentLadder = currentPath.CurrentNode.Ladders; - if (currentLadder != null && currentLadder.Item.NonInteractable) + if (currentLadder != null && !currentLadder.Item.IsInteractable(character)) { currentLadder = null; } @@ -303,7 +303,7 @@ namespace Barotrauma var ladders = currentLadder ?? nextLadder; if (canClimb && !isDiving && ladders != null && character.SelectedConstruction != ladders.Item) { - if (IsNextNodeLadder || currentPath.CurrentIndex == currentPath.Nodes.Count - 1) + if (IsNextNodeLadder || currentPath.Finished) { if (character.CanInteractWith(ladders.Item)) { @@ -361,7 +361,7 @@ namespace Barotrauma nextLadder.Item.TryInteract(character, false, true); } } - if (nextLadder != null || isAboveFloor) + if (isAboveFloor || nextLadderSameAsCurrent) { currentPath.SkipToNextNode(); } @@ -387,8 +387,7 @@ namespace Barotrauma character.SelectedConstruction = null; } var door = currentPath.CurrentNode.ConnectedDoor; - bool blockedByDoor = door != null && !door.IsOpen && !door.IsBroken; - if (!blockedByDoor) + if (door == null || door.CanBeTraversed) { float multiplier = MathHelper.Lerp(1, 10, MathHelper.Clamp(collider.LinearVelocity.Length() / 10, 0, 1)); float targetDistance = collider.GetSize().X * multiplier; @@ -421,10 +420,9 @@ namespace Barotrauma bool isAboveFeet = currentPath.CurrentNode.SimPosition.Y > colliderBottom.Y; bool isNotTooHigh = currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y + characterHeight; var door = currentPath.CurrentNode.ConnectedDoor; - bool blockedByDoor = door != null && !door.IsOpen && !door.IsBroken; float margin = MathHelper.Lerp(1, 10, MathHelper.Clamp(Math.Abs(velocity.X) / 10, 0, 1)); float targetDistance = Math.Max(collider.radius * margin, minWidth); - if (horizontalDistance < targetDistance && isAboveFeet && isNotTooHigh && !blockedByDoor) + if (horizontalDistance < targetDistance && isAboveFeet && isNotTooHigh && (door == null || door.CanBeTraversed)) { currentPath.SkipToNextNode(); } @@ -438,18 +436,20 @@ namespace Barotrauma private bool CanAccessDoor(Door door, Func buttonFilter = null) { - if (door.IsOpen) { return true; } - if (door.Item.NonInteractable) { return false; } - if (CanBreakDoors) { return true; } - if (door.IsStuck || door.IsJammed) { return false; } - if (!canOpenDoors || character.LockHands) { return false; } + if (door.IsOpen || door.IsBroken) { return true; } + if (!door.Item.IsInteractable(character)) { return false; } + if (!CanBreakDoors) + { + if (door.IsStuck || door.IsJammed) { return false; } + if (!canOpenDoors || character.LockHands) { return false; } + } if (door.HasIntegratedButtons) { - return door.CanBeOpenedWithoutTools(character); + return door.HasAccess(character) || CanBreakDoors; } else { - return door.Item.GetConnectedComponents(true).Any(b => !b.Item.NonInteractable && b.HasAccess(character) && (buttonFilter == null || buttonFilter(b))); + return door.Item.GetConnectedComponents(true).Any(b => b.HasAccess(character) && (buttonFilter == null || buttonFilter(b))) || CanBreakDoors; } } @@ -624,18 +624,19 @@ namespace Barotrauma } } + bool nextNodeAboveWaterLevel = nextNode.Waypoint.CurrentHull != null && nextNode.Waypoint.CurrentHull.Surface < nextNode.Waypoint.Position.Y; //non-humanoids can't climb up ladders if (!(character.AnimController is HumanoidAnimController)) { - if (node.Waypoint.Ladders != null && nextNode.Waypoint.Ladders != null && (nextNode.Waypoint.Ladders.Item.NonInteractable || character.LockHands)|| + if (node.Waypoint.Ladders != null && nextNode.Waypoint.Ladders != null && (!nextNode.Waypoint.Ladders.Item.IsInteractable(character) || character.LockHands)|| (nextNode.Position.Y - node.Position.Y > 1.0f && //more than one sim unit to climb up - nextNode.Waypoint.CurrentHull != null && nextNode.Waypoint.CurrentHull.Surface < nextNode.Waypoint.Position.Y)) //upper node not underwater + nextNodeAboveWaterLevel)) //upper node not underwater { return null; } } - if (node.Waypoint != null && node.Waypoint.CurrentHull != null) + if (node.Waypoint.CurrentHull != null) { var hull = node.Waypoint.CurrentHull; if (hull.FireSources.Count > 0) @@ -645,23 +646,26 @@ namespace Barotrauma penalty += fs.Size.X * 10.0f; } } - if (character.NeedsAir && hull.WaterVolume / hull.Rect.Width > 100.0f) + if (character.NeedsAir) { - if (!HumanAIController.HasDivingSuit(character)) + if (hull.WaterVolume / hull.Rect.Width > 100.0f) { - penalty += 500.0f; + if (!HumanAIController.HasDivingSuit(character)) + { + penalty += 500.0f; + } + } + if (character.PressureProtection < 10.0f && hull.WaterVolume > hull.Volume) + { + penalty += 1000.0f; } } - if (character.PressureProtection < 10.0f && hull.WaterVolume > hull.Volume) - { - penalty += 1000.0f; - } - } - float yDist = Math.Abs(node.Position.Y - nextNode.Position.Y); - if (node.Waypoint.Ladders == null && nextNode.Waypoint.Ladders == null) - { - penalty += yDist * 10.0f; + float yDist = Math.Abs(node.Position.Y - nextNode.Position.Y); + if (nextNodeAboveWaterLevel && node.Waypoint.Ladders == null && nextNode.Waypoint.Ladders == null && node.Waypoint.Stairs == null && nextNode.Waypoint.Stairs == null) + { + penalty += yDist * 10.0f; + } } return penalty; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs index 70eefcc41..9a822be02 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs @@ -178,7 +178,7 @@ namespace Barotrauma { if (Timing.TotalTime < GameMain.GameSession.RoundStartTime + 120.0f && speaker?.CurrentHull != null && - speaker.TeamID == Character.TeamType.FriendlyNPC && + speaker.TeamID == CharacterTeamType.FriendlyNPC && Character.CharacterList.Any(c => c.TeamID != speaker.TeamID && c.CurrentHull == speaker.CurrentHull)) { currentFlags.Add("EnterOutpost"); @@ -213,7 +213,7 @@ namespace Barotrauma } } - if (speaker.TeamID == Character.TeamType.FriendlyNPC && speaker.Submarine != null && speaker.Submarine.Info.IsOutpost) + if (speaker.TeamID == CharacterTeamType.FriendlyNPC && speaker.Submarine != null && speaker.Submarine.Info.IsOutpost) { currentFlags.Add("OutpostNPC"); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs index 9b3913f2a..502f2c559 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs @@ -220,7 +220,7 @@ namespace Barotrauma { if (!AllowOutsideSubmarine && character.Submarine == null) { return false; } if (AllowInAnySub) { return true; } - if (AllowInFriendlySubs && character.Submarine.TeamID == Character.TeamType.FriendlyNPC) { return true; } + if (AllowInFriendlySubs && character.Submarine.TeamID == CharacterTeamType.FriendlyNPC) { return true; } return character.Submarine.TeamID == character.TeamID || character.Submarine.DockedTo.Any(sub => sub.TeamID == character.TeamID); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs index efea317ca..5364a01b0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs @@ -21,7 +21,7 @@ namespace Barotrauma if (battery == null) { return false; } var item = battery.Item; if (item.IgnoreByAI) { return false; } - if (item.NonInteractable) { return false; } + if (!item.IsInteractable(character)) { return false; } if (item.Submarine == null) { return false; } if (item.CurrentHull == null) { return false; } if (item.Submarine.TeamID != character.TeamID) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs index feb1032b7..8c110d2ae 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs @@ -58,6 +58,11 @@ namespace Barotrauma { // Only continue when the get item sub objectives have been completed. if (subObjectives.Any()) { return; } + if (item.IgnoreByAI) + { + Abandon = true; + return; + } if (HumanAIController.FindSuitableContainer(character, item, ignoredContainers, ref itemIndex, out Item suitableContainer)) { itemIndex = 0; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs index 6d02eb2ec..f45c39440 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs @@ -12,15 +12,24 @@ namespace Barotrauma public override bool AllowAutomaticItemUnequipping => false; public override bool ForceOrderPriority => false; - public readonly Item prioritizedItem; + public readonly List prioritizedItems = new List(); - public AIObjectiveCleanupItems(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1, Item prioritizedItem = null) + public AIObjectiveCleanupItems(Character character, AIObjectiveManager objectiveManager, Item prioritizedItem = null, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { - this.prioritizedItem = prioritizedItem; + if (prioritizedItem != null) + { + prioritizedItems.Add(prioritizedItem); + } } - protected override float TargetEvaluation() => Targets.Any() ? AIObjectiveManager.RunPriority - 1 : 0; + public AIObjectiveCleanupItems(Character character, AIObjectiveManager objectiveManager, IEnumerable prioritizedItems, float priorityModifier = 1) + : base(character, objectiveManager, priorityModifier) + { + this.prioritizedItems.AddRange(prioritizedItems.Where(i => i != null)); + } + + protected override float TargetEvaluation() => Targets.Any() ? (objectiveManager.CurrentOrder == this ? AIObjectiveManager.OrderPriority : AIObjectiveManager.RunPriority - 1) : 0; protected override bool Filter(Item target) { @@ -38,7 +47,7 @@ namespace Barotrauma protected override AIObjective ObjectiveConstructor(Item item) => new AIObjectiveCleanupItem(item, character, objectiveManager, priorityModifier: PriorityModifier) { - IsPriority = prioritizedItem == item + IsPriority = prioritizedItems.Contains(item) }; protected override void OnObjectiveCompleted(AIObjective objective, Item target) @@ -56,12 +65,19 @@ namespace Barotrauma return true; } + public static bool IsValidContainer(Item item, Character character) => + !item.IgnoreByAI && item.IsInteractable(character) && item.HasTag("allowcleanup") && item.ParentInventory == null && item.OwnInventory != null && item.OwnInventory.AllItems.Any() && IsItemInsideValidSubmarine(item, character); + public static bool IsValidTarget(Item item, Character character, bool checkInventory) { if (item == null) { return false; } if (item.IgnoreByAI) { return false; } - if (item.NonInteractable) { return false; } - if (item.ParentInventory != null) { return false; } + if (!item.IsInteractable(character)) { return false; } + if (item.SpawnedInOutpost) { return false; } + if (item.ParentInventory != null) + { + if (item.Container == null || !IsValidContainer(item.Container, character)) { return false; } + } if (character != null && !IsItemInsideValidSubmarine(item, character)) { return false; } var pickable = item.GetComponent(); if (pickable == null) { return false; } @@ -96,18 +112,16 @@ namespace Barotrauma { foreach (var slotType in inv.SlotTypes) { - if (allowedSlot.HasFlag(slotType)) + if (!allowedSlot.HasFlag(slotType)) { continue; } + for (int i = 0; i < inv.Capacity; i++) { - for (int i = 0; i < inv.Capacity; i++) + canEquip = true; + if (allowedSlot.HasFlag(inv.SlotTypes[i]) && inv.GetItemAt(i) != null) { - canEquip = true; - if (allowedSlot.HasFlag(inv.SlotTypes[i]) && inv.Items[i] != null) - { - canEquip = false; - break; - } + canEquip = false; + break; } - } + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index 580f2e80d..245abc582 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -140,7 +140,7 @@ namespace Barotrauma public override float GetPriority() { - if (character.TeamID == Character.TeamType.FriendlyNPC && Enemy != null) + if (character.TeamID == CharacterTeamType.FriendlyNPC && Enemy != null) { if (Enemy.Submarine == null || (Enemy.Submarine.TeamID != character.TeamID && Enemy.Submarine != character.Submarine)) { @@ -238,7 +238,9 @@ namespace Barotrauma } } - private bool IsLoaded(ItemComponent weapon) => weapon.HasRequiredContainedItems(character, addMessage: false); + private bool IsLoaded(ItemComponent weapon, bool checkContainedItems = true) => + weapon.HasRequiredContainedItems(character, addMessage: false) && + (!checkContainedItems || weapon.Item.OwnInventory == null || weapon.Item.OwnInventory.AllItems.Any(i => i.Condition > 0)); private bool TryArm() { @@ -260,21 +262,21 @@ namespace Barotrauma // No weapons break; } - if (!character.Inventory.Items.Contains(Weapon) || WeaponComponent == null) + if (!character.Inventory.Contains(Weapon) || WeaponComponent == null) { // Not in the inventory anymore or cannot find the weapon component allWeapons.Remove(WeaponComponent); Weapon = null; continue; } - if (IsLoaded(WeaponComponent)) + if (IsLoaded(WeaponComponent, checkContainedItems: true)) { // All good, the weapon is loaded break; } if (Reload(seekAmmo: false)) { - // All good, reloading successful + // All good, we can use the weapon. break; } else @@ -304,7 +306,7 @@ namespace Barotrauma } } } - bool isAllowedToSeekWeapons = !EnemyIsClose() && character.TeamID != Character.TeamType.FriendlyNPC && IsOffensiveOrArrest; + bool isAllowedToSeekWeapons = !EnemyIsClose() && character.TeamID != CharacterTeamType.FriendlyNPC && IsOffensiveOrArrest; if (!isAllowedToSeekWeapons) { if (WeaponComponent == null) @@ -369,7 +371,7 @@ namespace Barotrauma bool CheckWeapon(bool seekAmmo) { - if (!character.Inventory.Items.Contains(Weapon) || WeaponComponent == null) + if (!character.Inventory.Contains(Weapon) || WeaponComponent == null) { // Not in the inventory anymore or cannot find the weapon component return false; @@ -564,21 +566,20 @@ namespace Barotrauma container.ContainableItems.Any(containable => containable.Identifiers.Any(id => id.Equals(mobileBatteryTag)))); // If there's no such container, assume that the melee weapon can stun without a battery. return containers.None() || containers.Any(container => - (container as ItemContainer)?.Inventory.Items.Any(i => i != null && i.HasTag(mobileBatteryTag) && i.Condition > 0.0f) ?? false); + (container as ItemContainer)?.Inventory.AllItems.Any(i => i != null && i.HasTag(mobileBatteryTag) && i.Condition > 0.0f) ?? false); } } private HashSet FindWeaponsFromInventory() { weapons.Clear(); - foreach (var item in character.Inventory.Items) + foreach (var item in character.Inventory.AllItems) { - if (item == null) { continue; } if (ignoredWeapons.Contains(item)) { continue; } GetWeapons(item, weapons); if (item.OwnInventory != null) { - item.OwnInventory.Items.ForEach(i => GetWeapons(i, weapons)); + item.OwnInventory.AllItems.ForEach(i => GetWeapons(i, weapons)); } } return weapons; @@ -598,7 +599,7 @@ namespace Barotrauma private void Unequip() { - if (!character.LockHands && character.SelectedItems.Contains(Weapon)) + if (!character.LockHands && character.HeldItems.Contains(Weapon)) { if (!Weapon.AllowedSlots.Contains(InvSlotType.Any) || !character.Inventory.TryPutItem(Weapon, character, new List() { InvSlotType.Any })) { @@ -617,7 +618,7 @@ namespace Barotrauma if (!character.HasEquippedItem(Weapon)) { Weapon.TryInteract(character, forceSelectKey: true); - var slots = Weapon.AllowedSlots.FindAll(s => s == InvSlotType.LeftHand || s == InvSlotType.RightHand || s == (InvSlotType.LeftHand | InvSlotType.RightHand)); + var slots = Weapon.AllowedSlots.Where(s => s == InvSlotType.LeftHand || s == InvSlotType.RightHand || s == (InvSlotType.LeftHand | InvSlotType.RightHand)); if (character.Inventory.TryPutItem(Weapon, character, slots)) { aimTimer = Rand.Range(0.5f, 1f); @@ -651,7 +652,7 @@ namespace Barotrauma } else { - retreatTarget = findSafety.FindBestHull(HumanAIController.VisibleHulls, allowChangingTheSubmarine: character.TeamID != Character.TeamType.FriendlyNPC); + retreatTarget = findSafety.FindBestHull(HumanAIController.VisibleHulls, allowChangingTheSubmarine: character.TeamID != CharacterTeamType.FriendlyNPC); findHullTimer = findHullInterval * Rand.Range(0.9f, 1.1f); } } @@ -724,7 +725,7 @@ namespace Barotrauma } else { - if (character.TeamID == Character.TeamType.FriendlyNPC) + if (character.TeamID == CharacterTeamType.FriendlyNPC) { ItemPrefab prefab = ItemPrefab.Find(null, "handcuffs"); if (prefab != null) @@ -769,9 +770,9 @@ namespace Barotrauma #endif } // Confiscate stolen goods. - foreach (var item in Enemy.Inventory.Items) + foreach (var item in Enemy.Inventory.AllItemsMod) { - if (item == null || item == handCuffs) { continue; } + if (item == handCuffs) { continue; } if (item.StolenDuringRound) { item.Drop(character); @@ -814,33 +815,50 @@ namespace Barotrauma /// private bool Reload(bool seekAmmo) { - if (WeaponComponent == null) { return false; } - if (!WeaponComponent.requiredItems.ContainsKey(RelatedItem.RelationType.Contained)) { return false; } - var containedItems = Weapon.OwnInventory?.Items; - if (containedItems == null) { return true; } - // Drop empty ammo - foreach (Item containedItem in containedItems) + if (WeaponComponent == null) { return false; } + if (Weapon.OwnInventory == null) { return true; } + // Eject empty ammo + if (Weapon.OwnInventory.AllItems.Any(it => it.Condition <= 0.0f)) { - if (containedItem == null) { continue; } - if (containedItem.Condition <= 0) + foreach (Item containedItem in Weapon.OwnInventory.AllItemsMod) { - containedItem.Drop(character); + if (containedItem.Condition <= 0) + { + if (character.Submarine == null) + { + // If we are outside of main sub, try to put the ammo in the inventory instead dropping it in the sea. + if (character.Inventory.TryPutItem(containedItem, character, CharacterInventory.anySlot)) + { + continue; + } + } + containedItem.Drop(character); + } } } + RelatedItem item = null; Item ammunition = null; string[] ammunitionIdentifiers = null; - foreach (RelatedItem requiredItem in WeaponComponent.requiredItems[RelatedItem.RelationType.Contained]) + if (WeaponComponent.requiredItems.ContainsKey(RelatedItem.RelationType.Contained)) { - ammunition = containedItems.FirstOrDefault(it => it != null && it.Condition > 0 && requiredItem.MatchesItem(it)); - if (ammunition != null) + foreach (RelatedItem requiredItem in WeaponComponent.requiredItems[RelatedItem.RelationType.Contained]) { - // Ammunition still remaining - return true; + ammunition = Weapon.OwnInventory.AllItems.FirstOrDefault(it => it.Condition > 0 && requiredItem.MatchesItem(it)); + if (ammunition != null) + { + // Ammunition still remaining + return true; + } + item = requiredItem; + ammunitionIdentifiers = requiredItem.Identifiers; } - item = requiredItem; - ammunitionIdentifiers = requiredItem.Identifiers; } + else if (WeaponComponent is MeleeWeapon meleeWeapon) + { + ammunitionIdentifiers = meleeWeapon.PreferredContainedItems; + } + // No ammo if (ammunition == null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs index 99f27021e..dbaa4c350 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs @@ -67,14 +67,14 @@ namespace Barotrauma } if (item != null) { - return container.Inventory.Items.Contains(item); + return container.Inventory.Contains(item); } else { int containedItemCount = 0; - foreach (Item i in container.Inventory.Items) + foreach (Item it in container.Inventory.AllItems) { - if (i != null && CheckItem(i)) + if (CheckItem(it)) { containedItemCount++; } @@ -83,7 +83,7 @@ namespace Barotrauma } } - private bool CheckItem(Item i) => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)) && i.ConditionPercentage >= ConditionLevel; + private bool CheckItem(Item i) => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)) && i.ConditionPercentage >= ConditionLevel && !i.IsThisOrAnyContainerIgnoredByAI(); protected override void Act(float deltaTime) { @@ -102,11 +102,10 @@ namespace Barotrauma } if (character.CanInteractWith(container.Item, checkLinked: false)) { - if (RemoveEmpty) + if (RemoveEmpty && container.Inventory.AllItems.Any(it => it.Condition <= 0.0f)) { - foreach (var emptyItem in container.Inventory.Items) + foreach (var emptyItem in container.Inventory.AllItemsMod) { - if (emptyItem == null) { continue; } if (emptyItem.Condition <= 0) { emptyItem.Drop(character); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs index b00e588cd..d0ba8e751 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs @@ -58,12 +58,17 @@ 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)), recursive: false); + Item itemToDecontain = targetItem ?? sourceContainer.Inventory.FindItem(i => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id) && !i.IgnoreByAI), recursive: false); if (itemToDecontain == null) { Abandon = true; return; } + if (itemToDecontain.IgnoreByAI) + { + Abandon = true; + return; + } if (targetContainer == null) { if (sourceContainer == null) @@ -77,7 +82,7 @@ namespace Barotrauma return; } } - else if (targetContainer.Inventory.Items.Contains(itemToDecontain)) + else if (targetContainer.Inventory.Contains(itemToDecontain)) { IsCompleted = true; return; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs index 0df695c01..2a685c43f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs @@ -111,9 +111,10 @@ namespace Barotrauma float xDist = Math.Abs(character.WorldPosition.X - fs.WorldPosition.X) - fs.DamageRange; float yDist = Math.Abs(character.WorldPosition.Y - fs.WorldPosition.Y); bool inRange = xDist + yDist < extinguisher.Range; - bool canSee = HumanAIController.VisibleHulls.Contains(fs.Hull) || character.CanSeeTarget(fs); - bool move = !inRange || !canSee; - if ((inRange && canSee) || useExtinquisherTimer > 0) + // Use the hull position, because the fire x pos is sometimes inside a wall -> the bot can't ever see it and continues running towards the wall. + ISpatialEntity lookTarget = character.CurrentHull == targetHull || character.CurrentHull.linkedTo.Contains(targetHull) ? targetHull : fs as ISpatialEntity; + bool move = !inRange || !character.CanSeeTarget(lookTarget); + if ((inRange && character.CanSeeTarget(lookTarget)) || useExtinquisherTimer > 0) { useExtinquisherTimer += deltaTime; if (useExtinquisherTimer > 2.0f) @@ -148,7 +149,7 @@ namespace Barotrauma onAbandon: () => Abandon = true, onCompleted: () => RemoveSubObjective(ref gotoObjective))) { - gotoObjective.requiredCondition = () => HumanAIController.VisibleHulls.Contains(fs.Hull); + gotoObjective.requiredCondition = () => targetHull == null || character.CanSeeTarget(targetHull); } } else diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs index 12518f633..b877fd2ce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs @@ -38,7 +38,6 @@ namespace Barotrauma public static bool IsValidTarget(Hull hull, Character character) { if (hull == null) { return false; } - if (hull.IgnoreByAI) { return false; } if (hull.FireSources.None()) { return false; } if (hull.Submarine == null) { return false; } if (character.Submarine == null) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs index 11cd94e7f..ef57dc156 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs @@ -26,7 +26,7 @@ namespace Barotrauma protected override AIObjective ObjectiveConstructor(Character target) { var combatObjective = new AIObjectiveCombat(character, target, AIObjectiveCombat.CombatMode.Offensive, objectiveManager, PriorityModifier); - if (character.TeamID == Character.TeamType.FriendlyNPC && target.TeamID == Character.TeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign) + if (character.TeamID == CharacterTeamType.FriendlyNPC && target.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign) { var reputation = campaign.Map?.CurrentLocation?.Reputation; if (reputation != null && reputation.NormalizedValue < Reputation.HostileThreshold) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs index fe13ffe97..bd4b4314a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs @@ -1,5 +1,6 @@ using Barotrauma.Items.Components; using Barotrauma.Extensions; +using System.Collections.Generic; namespace Barotrauma { @@ -56,7 +57,7 @@ namespace Barotrauma } else { - if (!DropEmptyTanks(character, targetItem, out Item[] containedItems)) + if (!EjectEmptyTanks(character, targetItem, out var containedItems)) { #if DEBUG DebugConsole.ThrowError($"{character.Name}: AIObjectiveFindDivingGear failed - the item \"" + targetItem + "\" has no proper inventory"); @@ -70,8 +71,11 @@ namespace Barotrauma // Seek oxygen that has min 10% condition left. TryAddSubObjective(ref getOxygen, () => { - character.Speak(TextManager.Get("DialogGetOxygenTank"), null, 0, "getoxygentank", 30.0f); - return new AIObjectiveContainItem(character, OXYGEN_SOURCE, targetItem.GetComponent(), objectiveManager, spawnItemIfNotFound: character.TeamID == Character.TeamType.FriendlyNPC) + if (!HumanAIController.HasItem(character, "oxygensource", out _, conditionPercentage: 10)) + { + character.Speak(TextManager.Get("DialogGetOxygenTank"), null, 0, "getoxygentank", 30.0f); + } + return new AIObjectiveContainItem(character, OXYGEN_SOURCE, targetItem.GetComponent(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC) { AllowToFindDivingGear = false, AllowDangerousPressure = true, @@ -83,7 +87,7 @@ namespace Barotrauma // Try to seek any oxygen sources. TryAddSubObjective(ref getOxygen, () => { - return new AIObjectiveContainItem(character, OXYGEN_SOURCE, targetItem.GetComponent(), objectiveManager, spawnItemIfNotFound: character.TeamID == Character.TeamType.FriendlyNPC) + return new AIObjectiveContainItem(character, OXYGEN_SOURCE, targetItem.GetComponent(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC) { AllowToFindDivingGear = false, AllowDangerousPressure = true @@ -100,18 +104,22 @@ namespace Barotrauma /// /// Returns false only when no inventory can be found from the item. /// - public static bool DropEmptyTanks(Character actor, Item target, out Item[] containedItems) + public static bool EjectEmptyTanks(Character actor, Item target, out IEnumerable containedItems) { - containedItems = target.OwnInventory?.Items; - if (containedItems == null) + containedItems = target.OwnInventory?.AllItems; + if (containedItems == null) { return false; } + foreach (Item containedItem in target.OwnInventory.AllItemsMod) { - return false; - } - foreach (Item containedItem in containedItems) - { - if (containedItem == null) { continue; } if (containedItem.Condition <= 0.0f) { + if (actor.Submarine == null) + { + // If we are outside of main sub, try to put the tank in the inventory instead dropping it in the sea. + if (actor.Inventory.TryPutItem(containedItem, actor, CharacterInventory.anySlot)) + { + continue; + } + } containedItem.Drop(actor); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs index 00db83826..191d16d99 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs @@ -168,7 +168,7 @@ namespace Barotrauma { searchHullTimer = SearchHullInterval * Rand.Range(0.9f, 1.1f); previousSafeHull = currentSafeHull; - currentSafeHull = FindBestHull(allowChangingTheSubmarine: character.TeamID != Character.TeamType.FriendlyNPC); + currentSafeHull = FindBestHull(allowChangingTheSubmarine: character.TeamID != CharacterTeamType.FriendlyNPC); cannotFindSafeHull = currentSafeHull == null || HumanAIController.NeedsDivingGear(currentSafeHull, out _); if (currentSafeHull == null) { @@ -359,7 +359,7 @@ namespace Barotrauma hullSafety *= distanceFactor; // If the target is not inside a friendly submarine, considerably reduce the hull safety. // Intentionally exclude wrecks from this check - if (hull.Submarine.TeamID != character.TeamID && hull.Submarine.TeamID != Character.TeamType.FriendlyNPC) + if (hull.Submarine.TeamID != character.TeamID && hull.Submarine.TeamID != CharacterTeamType.FriendlyNPC) { hullSafety /= 10; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs index ef05c4219..415158c6f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs @@ -64,7 +64,7 @@ namespace Barotrauma var weldingTool = character.Inventory.FindItemByTag("weldingequipment", true); if (weldingTool == null) { - TryAddSubObjective(ref getWeldingTool, () => new AIObjectiveGetItem(character, "weldingequipment", objectiveManager, equip: true, spawnItemIfNotFound: character.TeamID == Character.TeamType.FriendlyNPC), + TryAddSubObjective(ref getWeldingTool, () => new AIObjectiveGetItem(character, "weldingequipment", objectiveManager, equip: true, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC), onAbandon: () => { if (objectiveManager.IsCurrentOrder()) @@ -78,8 +78,7 @@ namespace Barotrauma } else { - var containedItems = weldingTool.OwnInventory?.Items; - if (containedItems == null) + if (weldingTool.OwnInventory == null) { #if DEBUG DebugConsole.ThrowError($"{character.Name}: AIObjectiveFixLeak failed - the item \"" + weldingTool + "\" has no proper inventory"); @@ -88,17 +87,20 @@ namespace Barotrauma return; } // Drop empty tanks - foreach (Item containedItem in containedItems) + if (weldingTool.OwnInventory.AllItems.Any(it => it.Condition <= 0.0f)) { - if (containedItem == null) { continue; } - if (containedItem.Condition <= 0.0f) + foreach (Item containedItem in weldingTool.OwnInventory.AllItemsMod) { - containedItem.Drop(character); + if (containedItem.Condition <= 0.0f) + { + containedItem.Drop(character); + } } } - if (containedItems.None(i => i != null && i.HasTag("weldingfuel") && i.Condition > 0.0f)) + + if (weldingTool.OwnInventory.AllItems.None(i => i.HasTag("weldingfuel") && i.Condition > 0.0f)) { - TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel", weldingTool.GetComponent(), objectiveManager, spawnItemIfNotFound: character.TeamID == Character.TeamType.FriendlyNPC), + TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel", weldingTool.GetComponent(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC), onAbandon: () => Abandon = true, onCompleted: () => RemoveSubObjective(ref refuelObjective)); return; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs index ccb23c9e3..1cd5d6b3d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs @@ -177,7 +177,7 @@ namespace Barotrauma } else if (moveToTarget is Item parentItem) { - canInteract = character.CanInteractWith(parentItem, out _, checkLinked: false); + canInteract = character.CanInteractWith(parentItem, checkLinked: false); } if (canInteract) { @@ -256,7 +256,7 @@ namespace Barotrauma if (mySub == null) { continue; } if (!AllowStealing) { - if (character.TeamID == Character.TeamType.FriendlyNPC != item.SpawnedInOutpost) { continue; } + if (character.TeamID == CharacterTeamType.FriendlyNPC != item.SpawnedInOutpost) { continue; } } if (!CheckItem(item)) { continue; } if (ignoredContainerIdentifiers != null && item.Container != null) @@ -276,6 +276,10 @@ namespace Barotrauma itemPriority = GetItemPriority(item); } Entity rootInventoryOwner = item.GetRootInventoryOwner(); + if (rootInventoryOwner is Item ownerItem) + { + if (!ownerItem.IsInteractable(character)) { continue; } + } Vector2 itemPos = (rootInventoryOwner ?? item).WorldPosition; float yDist = Math.Abs(character.WorldPosition.Y - itemPos.Y); yDist = yDist > 100 ? yDist * 5 : 0; @@ -308,7 +312,7 @@ namespace Barotrauma Entity.Spawner.AddToSpawnQueue(prefab, character.Inventory, onSpawned: (Item spawnedItem) => { targetItem = spawnedItem; - if (character.TeamID == Character.TeamType.FriendlyNPC && (character.Submarine?.Info.IsOutpost ?? false)) + if (character.TeamID == CharacterTeamType.FriendlyNPC && (character.Submarine?.Info.IsOutpost ?? false)) { spawnedItem.SpawnedInOutpost = true; } @@ -347,7 +351,7 @@ namespace Barotrauma private bool CheckItem(Item item) { - if (item.NonInteractable) { return false; } + if (!item.IsInteractable(character)) { return false; } if (item.IsThisOrAnyContainerIgnoredByAI()) { return false; } if (ignoredItems.Contains(item)) { return false; }; if (item.Condition < TargetCondition) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs index 1f3cac5fd..ed776cb32 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Barotrauma.Extensions; namespace Barotrauma { @@ -34,6 +35,9 @@ namespace Barotrauma public float extraDistanceOutsideSub; private float _closeEnough = 50; private readonly float minDistance = 50; + private readonly float seekGapsInterval = 1; + private float seekGapsTimer; + /// /// Display units /// @@ -116,6 +120,8 @@ namespace Barotrauma return Priority; } + private readonly float avoidLookAheadDistance = 5; + public AIObjectiveGoTo(ISpatialEntity target, Character character, AIObjectiveManager objectiveManager, bool repeat = false, bool getDivingGearIfNeeded = true, float priorityModifier = 1, float closeEnough = 0) : base(character, objectiveManager, priorityModifier) { @@ -208,14 +214,14 @@ namespace Barotrauma { Abandon = true; } - else if (waitUntilPathUnreachable < 0) + else if (SteeringManager == PathSteering && PathSteering.CurrentPath != null && PathSteering.CurrentPath.Unreachable && !PathSteering.IsPathDirty) { - if (SteeringManager == PathSteering && PathSteering.CurrentPath != null && PathSteering.CurrentPath.Unreachable && !PathSteering.IsPathDirty) + SteeringManager.Reset(); + if (waitUntilPathUnreachable < 0) { if (repeat) { SpeakCannotReach(); - SteeringManager.Reset(); } else { @@ -223,12 +229,7 @@ namespace Barotrauma } } } - if (Abandon) - { - SpeakCannotReach(); - SteeringManager.Reset(); - } - else + if (!Abandon) { if (getDivingGearIfNeeded && !character.LockHands) { @@ -286,52 +287,134 @@ namespace Barotrauma } } } - if (!character.AnimController.InWater) + if (character.AnimController.InWater) { - useScooter = false; - checkScooterTimer = 0; - } - else if (checkScooterTimer <= 0) - { - useScooter = false; - checkScooterTimer = checkScooterTime; - string scooterTag = "scooter"; - string batteryTag = "mobilebattery"; - Item scooter = null; - bool isScooterEquipped = false; - float closeEnough = 250; - float squaredDistance = Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition); - bool shouldUseScooter = squaredDistance > closeEnough * closeEnough && (!mimic || - (Target is Character targetCharacter && targetCharacter.HasEquippedItem(scooterTag, allowBroken: false)) || squaredDistance > Math.Pow(closeEnough * 2, 2)); - if (HumanAIController.HasItem(character, scooterTag, out IEnumerable equippedScooters, batteryTag, requireEquipped: true)) + if (character.CurrentHull == null) { - scooter = equippedScooters.FirstOrDefault(); - isScooterEquipped = scooter != null; - } - else if (shouldUseScooter && HumanAIController.HasItem(character, scooterTag, out IEnumerable scooters, batteryTag, requireEquipped: false)) - { - scooter = scooters.FirstOrDefault(); - if (scooter != null) + if (seekGapsTimer > 0) { - isScooterEquipped = HumanAIController.TakeItem(scooter, character.Inventory, equip: true, dropOtherIfCannotMove: false, allowSwapping: true, storeUnequipped: false); - } - } - if (scooter != null && isScooterEquipped) - { - if (shouldUseScooter) - { - useScooter = true; + seekGapsTimer -= deltaTime; } else { - // Unequip - character.Inventory.TryPutItem(scooter, character, CharacterInventory.anySlot); + SeekGaps(maxDistance: 500); + seekGapsTimer = seekGapsInterval * Rand.Range(0.1f, 1.1f); + if (TargetGap != null) + { + // Check that nothing is blocking the way + Vector2 rayStart = character.SimPosition; + Vector2 rayEnd = TargetGap.SimPosition; + if (TargetGap.Submarine != null && character.Submarine == null) + { + rayStart -= TargetGap.Submarine.SimPosition; + } + else if (TargetGap.Submarine == null && character.Submarine != null) + { + rayEnd -= character.Submarine.SimPosition; + } + var closestBody = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true); + if (closestBody != null) + { + TargetGap = null; + } + } } } + else + { + TargetGap = null; + } + if (TargetGap != null) + { + if (TargetGap.FlowTargetHull != null && HumanAIController.SteerThroughGap(TargetGap, TargetGap.FlowTargetHull.WorldPosition, deltaTime)) + { + SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 1); + return; + } + else + { + TargetGap = null; + } + } + if (checkScooterTimer <= 0) + { + useScooter = false; + checkScooterTimer = checkScooterTime; + string scooterTag = "scooter"; + string batteryTag = "mobilebattery"; + Item scooter = null; + float closeEnough = 250; + float squaredDistance = Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition); + bool shouldUseScooter = squaredDistance > closeEnough * closeEnough && (!mimic || + (Target is Character targetCharacter && targetCharacter.HasEquippedItem(scooterTag, allowBroken: false)) || squaredDistance > Math.Pow(closeEnough * 2, 2)); + if (HumanAIController.HasItem(character, scooterTag, out IEnumerable equippedScooters, recursive: false, requireEquipped: true)) + { + // Currently equipped scooter + scooter = equippedScooters.FirstOrDefault(); + } + else if (shouldUseScooter) + { + bool hasBattery = false; + if (HumanAIController.HasItem(character, scooterTag, out IEnumerable nonEquippedScooters, containedTag: batteryTag, conditionPercentage: 1, requireEquipped: false)) + { + // Non-equipped scooter with a battery + scooter = nonEquippedScooters.FirstOrDefault(); + hasBattery = true; + } + else if (HumanAIController.HasItem(character, scooterTag, out IEnumerable _nonEquippedScooters, requireEquipped: false)) + { + // Non-equipped scooter without a battery + scooter = _nonEquippedScooters.FirstOrDefault(); + // Non-recursive so that the bots won't take batteries from other items. Also means that they can't find batteries inside containers. Not sure how to solve this. + hasBattery = HumanAIController.HasItem(character, batteryTag, out _, requireEquipped: false, conditionPercentage: 1, recursive: false); + } + if (scooter != null && hasBattery) + { + // Equip only if we have a battery available + HumanAIController.TakeItem(scooter, character.Inventory, equip: true, dropOtherIfCannotMove: false, allowSwapping: true, storeUnequipped: false); + } + } + bool isScooterEquipped = scooter != null && character.HasEquippedItem(scooter); + if (scooter != null && isScooterEquipped) + { + if (shouldUseScooter) + { + useScooter = true; + // Check the battery + if (scooter.ContainedItems.None(i => i.Condition > 0)) + { + // Try to switch batteries + if (HumanAIController.HasItem(character, batteryTag, out IEnumerable batteries, conditionPercentage: 1, recursive: false)) + { + scooter.ContainedItems.ForEachMod(emptyBattery => character.Inventory.TryPutItem(emptyBattery, character, CharacterInventory.anySlot)); + if (!scooter.Combine(batteries.OrderByDescending(b => b.Condition).First(), character)) + { + useScooter = false; + } + } + else + { + useScooter = false; + } + } + } + if (!useScooter) + { + // Unequip + character.Inventory.TryPutItem(scooter, character, CharacterInventory.anySlot); + } + } + } + else + { + checkScooterTimer -= deltaTime; + } } else { - checkScooterTimer -= deltaTime; + TargetGap = null; + useScooter = false; + checkScooterTimer = 0; } if (SteeringManager == PathSteering) { @@ -347,7 +430,7 @@ namespace Barotrauma nodeFilter, CheckVisibility); - if (!isInside && PathSteering.CurrentPath == null || PathSteering.IsPathDirty || PathSteering.CurrentPath.Unreachable) + if (!isInside && (PathSteering.CurrentPath == null || PathSteering.IsPathDirty || PathSteering.CurrentPath.Unreachable)) { if (useScooter) { @@ -358,7 +441,7 @@ namespace Barotrauma SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(Target.WorldPosition - character.WorldPosition)); if (character.AnimController.InWater) { - SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: 5, weight: 2); + SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 2); } } } @@ -378,7 +461,7 @@ namespace Barotrauma SteeringManager.SteeringSeek(character.GetRelativeSimPosition(Target), 10); if (character.AnimController.InWater) { - SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: 5, weight: 15); + SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 15); } } } @@ -439,6 +522,27 @@ namespace Barotrauma return null; } + public Gap TargetGap { get; private set; } + private void SeekGaps(float maxDistance) + { + Gap selectedGap = null; + float selectedDistance = -1; + foreach (Gap gap in Gap.GapList) + { + if (gap.Open < 1) { continue; } + if (gap.FlowTargetHull == null) { continue; } + if (gap.Submarine != Target.Submarine) { continue; } + float distance = Vector2.DistanceSquared(character.WorldPosition, gap.WorldPosition); + if (distance > maxDistance * maxDistance) { continue; } + if (selectedGap == null || distance < selectedDistance) + { + selectedGap = gap; + selectedDistance = distance; + } + } + TargetGap = selectedGap; + } + public bool IsCloseEnough { get @@ -507,6 +611,7 @@ namespace Barotrauma { PathSteering.ResetPath(); } + SpeakCannotReach(); base.OnAbandon(); } @@ -530,6 +635,8 @@ namespace Barotrauma { base.Reset(); findDivingGear = null; + seekGapsTimer = 0; + TargetGap = null; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs index 1ced9115f..ae690ab40 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs @@ -21,7 +21,7 @@ namespace Barotrauma set { behavior = value; - if (behavior == BehaviorType.StayInHull && character.TeamID != Character.TeamType.FriendlyNPC) + if (behavior == BehaviorType.StayInHull && character.TeamID != CharacterTeamType.FriendlyNPC) { DebugConsole.NewMessage($"AIObjectiveIdle.BehaviorType.StayInHull is implemented only for outpost NPCs. Using passive behavior for {character.Name} ({character.Info.Job.Prefab.Identifier})", color: Color.Red); behavior = BehaviorType.Passive; @@ -203,7 +203,7 @@ namespace Barotrauma if (currentTarget != null && !currentTargetIsInvalid) { - if (character.TeamID == Character.TeamType.FriendlyNPC) + if (character.TeamID == CharacterTeamType.FriendlyNPC) { if (currentTarget.Submarine.TeamID != character.TeamID) { @@ -260,7 +260,7 @@ namespace Barotrauma { //choose a random available hull currentTarget = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced); - bool isInWrongSub = character.TeamID == Character.TeamType.FriendlyNPC && character.Submarine.TeamID != character.TeamID; + bool isInWrongSub = character.TeamID == CharacterTeamType.FriendlyNPC && character.Submarine.TeamID != character.TeamID; bool isCurrentHullAllowed = !isInWrongSub && !IsForbidden(character.CurrentHull); var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, errorMsgStr: $"AIObjectiveIdle {character.DisplayName}", nodeFilter: node => { @@ -402,6 +402,14 @@ namespace Barotrauma PathSteering.Wander(deltaTime); } + public void FaceTargetAndWait(ISpatialEntity target, float waitTime) + { + standStillTimer = waitTime; + HumanAIController.FaceTarget(target); + currentTarget = null; + SetTargetTimerHigh(); + } + private void FindTargetHulls() { targetHulls.Clear(); @@ -411,7 +419,7 @@ namespace Barotrauma if (HumanAIController.UnsafeHulls.Contains(hull)) { continue; } if (hull.Submarine == null) { continue; } if (character.Submarine == null) { break; } - if (character.TeamID == Character.TeamType.FriendlyNPC) + if (character.TeamID == CharacterTeamType.FriendlyNPC) { if (hull.Submarine.TeamID != character.TeamID) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs index 56b5c4ac8..f5d6ff93d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs @@ -127,10 +127,14 @@ namespace Barotrauma { var orderPrefab = Order.GetPrefab(autonomousObjective.identifier); if (orderPrefab == null) { throw new Exception($"Could not find a matching prefab by the identifier: '{autonomousObjective.identifier}'"); } - var item = orderPrefab.MustSetTarget ? orderPrefab.GetMatchingItems(character.Submarine, mustBelongToPlayerSub: false, requiredTeam: character.Info.TeamID)?.GetRandom() : null; + Item item = null; + if (orderPrefab.MustSetTarget) + { + item = orderPrefab.GetMatchingItems(character.Submarine, mustBelongToPlayerSub: false, requiredTeam: character.Info.TeamID, interactableFor: character)?.GetRandom(); + } var order = new Order(orderPrefab, item ?? character.CurrentHull as Entity, orderPrefab.GetTargetItemComponent(item), orderGiver: character); if (order == null) { continue; } - if (autonomousObjective.ignoreAtOutpost && Level.IsLoadedOutpost && character.TeamID != Character.TeamType.FriendlyNPC) { continue; } + if (autonomousObjective.ignoreAtOutpost && Level.IsLoadedOutpost && character.TeamID != CharacterTeamType.FriendlyNPC) { continue; } var objective = CreateObjective(order, autonomousObjective.option, character, isAutonomous: true, autonomousObjective.priorityModifier); if (objective != null && objective.CanBeCompleted) { @@ -273,7 +277,8 @@ namespace Barotrauma CurrentOrder = objective; } - public void SetOrder(Order order, string option, Character orderGiver) + private CoroutineHandle speakRoutine; + public void SetOrder(Order order, string option, Character orderGiver, bool speak) { if (character.IsDead) { @@ -294,6 +299,49 @@ namespace Barotrauma { // This should be redundant, because all the objectives are reset when they are selected as active. CurrentOrder.Reset(); + if (speak) + { + character.Speak(TextManager.Get("DialogAffirmative"), null, 1.0f); + if (speakRoutine != null) + { + CoroutineManager.StopCoroutines(speakRoutine); + } + speakRoutine = CoroutineManager.InvokeAfter(() => + { + if (GameMain.GameSession == null || Level.Loaded == null) { return; } + if (CurrentOrder != null && character.SpeechImpediment < 100.0f) + { + if (CurrentOrder is AIObjectiveRepairItems repairItems && repairItems.Targets.None()) + { + character.Speak(TextManager.Get("DialogNoRepairTargets"), null, 3.0f, "norepairtargets"); + } + else if (CurrentOrder is AIObjectiveChargeBatteries chargeBatteries && chargeBatteries.Targets.None()) + { + character.Speak(TextManager.Get("DialogNoBatteries"), null, 3.0f, "nobatteries"); + } + else if (CurrentOrder is AIObjectiveExtinguishFires extinguishFires && extinguishFires.Targets.None()) + { + character.Speak(TextManager.Get("DialogNoFire"), null, 3.0f, "nofire"); + } + else if (CurrentOrder is AIObjectiveFixLeaks fixLeaks && fixLeaks.Targets.None()) + { + character.Speak(TextManager.Get("DialogNoLeaks"), null, 3.0f, "noleaks"); + } + else if (CurrentOrder is AIObjectiveFightIntruders fightIntruders && fightIntruders.Targets.None()) + { + character.Speak(TextManager.Get("DialogNoEnemies"), null, 3.0f, "noenemies"); + } + else if (CurrentOrder is AIObjectiveRescueAll rescueAll && rescueAll.Targets.None()) + { + character.Speak(TextManager.Get("DialogNoRescueTargets"), null, 3.0f, "norescuetargets"); + } + else if (CurrentOrder is AIObjectivePumpWater pumpWater && pumpWater.Targets.None()) + { + character.Speak(TextManager.Get("DialogNoPumps"), null, 3.0f, "nopumps"); + } + } + }, 3); + } } } @@ -320,8 +368,7 @@ namespace Barotrauma case "wait": newObjective = new AIObjectiveGoTo(order.TargetSpatialEntity ?? character, character, this, repeat: true, priorityModifier: priorityModifier) { - AllowGoingOutside = order.TargetSpatialEntity == null ? character.CurrentHull == null : - character.Submarine == null || character.Submarine != order.TargetSpatialEntity.Submarine + AllowGoingOutside = character.Submarine == null || (order.TargetSpatialEntity != null && character.Submarine != order.TargetSpatialEntity.Submarine) }; break; case "fixleaks": @@ -345,7 +392,7 @@ namespace Barotrauma case "pumpwater": if (order.TargetItemComponent is Pump targetPump) { - if (order.TargetItemComponent.Item.NonInteractable) { return null; } + if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; } newObjective = new AIObjectiveOperateItem(targetPump, character, this, option, false, priorityModifier: priorityModifier) { IsLoop = true, @@ -370,7 +417,7 @@ namespace Barotrauma var steering = (order?.TargetEntity as Item)?.GetComponent(); if (steering != null) { steering.PosToMaintain = steering.Item.Submarine?.WorldPosition; } if (order.TargetItemComponent == null) { return null; } - if (order.TargetItemComponent.Item.NonInteractable) { return null; } + if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; } newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option, requireEquip: false, useController: order.UseController, controller: order.ConnectedController, priorityModifier: priorityModifier) { @@ -403,11 +450,26 @@ namespace Barotrauma }; break; case "cleanupitems": - newObjective = new AIObjectiveCleanupItems(character, this, priorityModifier, order.TargetEntity as Item); + if (order.TargetEntity is Item targetItem) + { + if (targetItem.HasTag("allowcleanup") && targetItem.ParentInventory == null && targetItem.OwnInventory != null) + { + // Target all items inside the container + newObjective = new AIObjectiveCleanupItems(character, this, targetItem.OwnInventory.AllItems, priorityModifier); + } + else + { + newObjective = new AIObjectiveCleanupItems(character, this, targetItem, priorityModifier); + } + } + else + { + newObjective = new AIObjectiveCleanupItems(character, this, priorityModifier: priorityModifier); + } break; default: if (order.TargetItemComponent == null) { return null; } - if (order.TargetItemComponent.Item.NonInteractable) { return null; } + if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; } newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option, requireEquip: false, useController: order.UseController, controller: order.ConnectedController, priorityModifier: priorityModifier) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs index 0a9f3505d..1eff075eb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs @@ -69,7 +69,7 @@ namespace Barotrauma { if (!isOrder) { - if (reactor.LastUserWasPlayer && character.TeamID != Character.TeamType.FriendlyNPC || + if (reactor.LastUserWasPlayer && character.TeamID != CharacterTeamType.FriendlyNPC || HumanAIController.IsTrueForAnyCrewMember(c => c.ObjectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder.GetTarget() == target)) { @@ -101,7 +101,8 @@ namespace Barotrauma targetItem.Submarine != character.Submarine && !isOrder || targetItem.CurrentHull.FireSources.Any() || HumanAIController.IsItemOperatedByAnother(target, out _) || - Character.CharacterList.Any(c => c.CurrentHull == targetItem.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) + Character.CharacterList.Any(c => c.CurrentHull == targetItem.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c)) + || component.Item.IgnoreByAI || (useController && controller.Item.IgnoreByAI)) { Priority = 0; } @@ -137,7 +138,7 @@ namespace Barotrauma throw new Exception("target null"); #endif } - else if (target.Item.NonInteractable) + else if (!target.Item.IsInteractable(character)) { Abandon = true; } @@ -157,21 +158,6 @@ namespace Barotrauma Abandon = true; return; } - // If this is not an order... - if (objectiveManager.CurrentOrder != this) - { - // Don't allow to operate an item that someone with a better skills already operates - if (HumanAIController.IsItemOperatedByAnother(target, out _)) - { - // Don't abandon - return; - } - if (component.Item.IgnoreByAI || (useController && controller.Item.IgnoreByAI)) - { - Abandon = true; - return; - } - } if (operateTarget != null) { if (HumanAIController.IsTrueForAnyCrewMember(other => other != HumanAIController && other.ObjectiveManager.GetActiveObjective() is AIObjectiveOperateItem operateObjective && operateObjective.operateTarget == operateTarget)) @@ -215,7 +201,7 @@ namespace Barotrauma Abandon = true; return; } - else if (!character.Inventory.Items.Contains(component.Item)) + else if (!character.Inventory.Contains(component.Item)) { TryAddSubObjective(ref getItemObjective, () => new AIObjectiveGetItem(character, component.Item, objectiveManager, equip: true), onAbandon: () => Abandon = true, @@ -241,13 +227,14 @@ namespace Barotrauma continue; } //equip slot already taken - if (character.Inventory.Items[i] != null) + var existingItem = character.Inventory.GetItemAt(i); + if (existingItem != null) { //try to put the item in an Any slot, and drop it if that fails - if (!character.Inventory.Items[i].AllowedSlots.Contains(InvSlotType.Any) || - !character.Inventory.TryPutItem(character.Inventory.Items[i], character, new List() { InvSlotType.Any })) + if (!existingItem.AllowedSlots.Contains(InvSlotType.Any) || + !character.Inventory.TryPutItem(existingItem, character, new List() { InvSlotType.Any })) { - character.Inventory.Items[i].Drop(character); + existingItem.Drop(character); } } if (character.Inventory.TryPutItem(component.Item, i, true, false, character)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs index 14a30c4ad..22a08c997 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs @@ -28,7 +28,7 @@ namespace Barotrauma { if (pump == null) { return false; } if (pump.Item.IgnoreByAI) { return false; } - if (pump.Item.NonInteractable) { return false; } + if (!pump.Item.IsInteractable(character)) { return false; } if (pump.Item.HasTag("ballast")) { return false; } if (pump.Item.Submarine == null) { return false; } if (pump.Item.CurrentHull == null) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs index 5d0c597fd..738de3d32 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs @@ -33,10 +33,14 @@ namespace Barotrauma public override float GetPriority() { - if (!IsAllowed) + if (!IsAllowed || Item.IgnoreByAI) { Priority = 0; Abandon = true; + if (IsRepairing()) + { + Item.Repairables.ForEach(r => r.StopRepairing(character)); + } return Priority; } // TODO: priority list? @@ -107,8 +111,7 @@ namespace Barotrauma } if (repairTool != null) { - var containedItems = repairTool.Item.OwnInventory?.Items; - if (containedItems == null) + if (repairTool.Item.OwnInventory == null) { #if DEBUG DebugConsole.ThrowError($"{character.Name}: AIObjectiveRepairItem failed - the item \"" + repairTool + "\" has no proper inventory"); @@ -116,27 +119,39 @@ namespace Barotrauma Abandon = true; return; } - // Drop empty tanks - foreach (Item containedItem in containedItems) + // Eject empty tanks + if (repairTool.Item.OwnInventory.AllItems.Any(it => it.Condition <= 0.0f)) { - if (containedItem == null) { continue; } - if (containedItem.Condition <= 0.0f) + foreach (Item containedItem in repairTool.Item.OwnInventory.AllItemsMod) { - containedItem.Drop(character); + if (containedItem == null) { continue; } + if (containedItem.Condition <= 0.0f) + { + if (character.Submarine == null) + { + // If we are outside of main sub, try to put the tank in the inventory instead dropping it in the sea. + if (character.Inventory.TryPutItem(containedItem, character, CharacterInventory.anySlot)) + { + continue; + } + } + containedItem.Drop(character); + } } } + RelatedItem item = null; Item fuel = null; foreach (RelatedItem requiredItem in repairTool.requiredItems[RelatedItem.RelationType.Contained]) { item = requiredItem; - fuel = containedItems.FirstOrDefault(it => it != null && it.Condition > 0.0f && requiredItem.MatchesItem(it)); + fuel = repairTool.Item.OwnInventory.AllItems.FirstOrDefault(it => it.Condition > 0.0f && requiredItem.MatchesItem(it)); if (fuel != null) { break; } } if (fuel == null) { RemoveSubObjective(ref goToObjective); - TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, item.Identifiers, repairTool.Item.GetComponent(), objectiveManager, spawnItemIfNotFound: character.TeamID == Character.TeamType.FriendlyNPC), + TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, item.Identifiers, repairTool.Item.GetComponent(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC), onCompleted: () => RemoveSubObjective(ref refuelObjective), onAbandon: () => Abandon = true); return; @@ -229,7 +244,7 @@ namespace Barotrauma { foreach (RelatedItem requiredItem in kvp.Value) { - foreach (var item in character.Inventory.Items) + foreach (var item in character.Inventory.AllItems) { if (requiredItem.MatchesItem(item)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs index 9f0588f44..aa3f65c4a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs @@ -149,7 +149,7 @@ namespace Barotrauma { if (item == null) { return false; } if (item.IgnoreByAI) { return false; } - if (item.NonInteractable) { return false; } + if (!item.IsInteractable(character)) { return false; } if (item.IsFullCondition) { return false; } if (item.CurrentHull == null) { return false; } if (item.Submarine == null || character.Submarine == null) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs index ebe7eb229..f17b9accd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs @@ -85,7 +85,7 @@ namespace Barotrauma Item suit = suits.FirstOrDefault(); if (suit != null) { - AIObjectiveFindDivingGear.DropEmptyTanks(character, suit, out _); + AIObjectiveFindDivingGear.EjectEmptyTanks(character, suit, out _); } } else if (HumanAIController.HasItem(targetCharacter, AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR, out IEnumerable masks, requireEquipped: true)) @@ -93,7 +93,7 @@ namespace Barotrauma Item mask = masks.FirstOrDefault(); if (mask != null) { - AIObjectiveFindDivingGear.DropEmptyTanks(character, mask, out _); + AIObjectiveFindDivingGear.EjectEmptyTanks(character, mask, out _); } } bool ShouldRemoveDivingSuit() => targetCharacter.OxygenAvailable < CharacterHealth.InsufficientOxygenThreshold && targetCharacter.CurrentHull?.LethalPressure <= 0; @@ -101,7 +101,7 @@ namespace Barotrauma { suits.ForEach(suit => suit.Drop(character)); } - else if (suits.Any() && suits.None(s => s.OwnInventory?.Items != null && s.OwnInventory.Items.Any(it => it != null && it.HasTag(AIObjectiveFindDivingGear.OXYGEN_SOURCE) && it.ConditionPercentage > 0))) + else if (suits.Any() && suits.None(s => s.OwnInventory?.AllItems != null && s.OwnInventory.AllItems.Any(it => it.HasTag(AIObjectiveFindDivingGear.OXYGEN_SOURCE) && it.ConditionPercentage > 0))) { // The target has a suit equipped with an empty oxygen tank. // Can't remove the suit, because the target needs it. @@ -331,9 +331,13 @@ namespace Barotrauma character.DeselectCharacter(); RemoveSubObjective(ref getItemObjective); TryAddSubObjective(ref getItemObjective, - constructor: () => new AIObjectiveGetItem(character, suitableItemIdentifiers.ToArray(), objectiveManager, equip: true, spawnItemIfNotFound: character.TeamID == Character.TeamType.FriendlyNPC), + constructor: () => new AIObjectiveGetItem(character, suitableItemIdentifiers.ToArray(), objectiveManager, equip: true, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC), onCompleted: () => RemoveSubObjective(ref getItemObjective), - onAbandon: () => RemoveSubObjective(ref getItemObjective)); + onAbandon: () => + { + Abandon = true; + character.Speak(TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, formatCapitals: false), identifier: "cannottreatpatient", minDurationBetweenSimilar: 20.0f); + }); } } } @@ -427,6 +431,13 @@ namespace Barotrauma replaceOxygenObjective = null; safeHull = null; ignoreOxygen = false; + character.SelectedCharacter = null; + } + + public override void OnDeselected() + { + character.SelectedCharacter = null; + base.OnDeselected(); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs index b6a52e39c..11a7cfb38 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs @@ -14,7 +14,7 @@ namespace Barotrauma public override bool AllowInAnySub => true; private const float vitalityThreshold = 75; - private const float vitalityThresholdForOrders = 85; + private const float vitalityThresholdForOrders = 90; public static float GetVitalityThreshold(AIObjectiveManager manager, Character character, Character target) { if (manager == null) @@ -23,7 +23,10 @@ namespace Barotrauma } else { - return character == target || manager.CurrentOrder is AIObjectiveRescueAll ? vitalityThresholdForOrders : vitalityThreshold; + // When targeting player characters, always treat them when ordered, else use the threshold so that minor/non-severe damage is ignored. + // If we ignore any damage when the player orders a bot to do healings, it's observed to cause confusion among the players. + // On the other hand, if the bots too eagerly heal characters when it's not nevessary, it's inefficient and can feel frustrating, because it can't be controlled. + return character == target || manager.CurrentOrder is AIObjectiveRescueAll ? (target.IsPlayer ? 100 : vitalityThresholdForOrders) : vitalityThreshold; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs index 923924892..0a970aca9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs @@ -59,6 +59,10 @@ namespace Barotrauma public Order Prefab { get; private set; } public readonly string Name; + /// + /// Name that can be used with the contextual version of the order + /// + public readonly string ContextualName; public readonly Sprite SymbolSprite; @@ -97,7 +101,6 @@ namespace Barotrauma public bool TargetAllCharacters { get; } public bool IsReport => TargetAllCharacters && !MustSetTarget; - public readonly float FadeOutTime; public Entity TargetEntity; @@ -119,9 +122,9 @@ namespace Barotrauma private readonly Dictionary minimapIcons; public Dictionary MinimapIcons => IsPrefab ? minimapIcons : Prefab.minimapIcons; - public readonly float Weight; public readonly bool MustSetTarget; public readonly string AppropriateSkill; + public readonly bool Hidden; public bool HasOptions => (IsPrefab ? Options : Prefab.Options).Length > 1; public bool IsPrefab { get; private set; } @@ -159,6 +162,11 @@ namespace Barotrauma public int? WallSectionIndex { get; } public bool IsIgnoreOrder { get; } + /// + /// Should the order icon be drawn when the order target is inside a container + /// + public bool DrawIconWhenContained { get; } + public static void Init() { Prefabs = new Dictionary(); @@ -239,7 +247,8 @@ namespace Barotrauma private Order(XElement orderElement) { Identifier = orderElement.GetAttributeString("identifier", ""); - Name = TextManager.Get("OrderName." + Identifier, true) ?? "Name not found"; + Name = TextManager.Get("OrderName." + Identifier, returnNull: true) ?? "Name not found"; + ContextualName = TextManager.Get("OrderNameContextual." + Identifier, returnNull: true) ?? Name; string targetItemType = orderElement.GetAttributeString("targetitemtype", ""); if (!string.IsNullOrWhiteSpace(targetItemType)) @@ -267,6 +276,7 @@ namespace Barotrauma if (!string.IsNullOrWhiteSpace(category)) { this.Category = (OrderCategory)Enum.Parse(typeof(OrderCategory), category, true); } MustSetTarget = orderElement.GetAttributeBool("mustsettarget", false); AppropriateSkill = orderElement.GetAttributeString("appropriateskill", null); + Hidden = orderElement.GetAttributeBool("hidden", false); var optionNames = TextManager.Get("OrderOptions." + Identifier, true)?.Split(',', ',') ?? orderElement.GetAttributeStringArray("optionnames", new string[0]); @@ -315,6 +325,7 @@ namespace Barotrauma IsPrefab = true; MustManuallyAssign = orderElement.GetAttributeBool("mustmanuallyassign", false); IsIgnoreOrder = Identifier == "ignorethis" || Identifier == "unignorethis"; + DrawIconWhenContained = orderElement.GetAttributeBool("displayiconwhencontained", false); } /// @@ -324,23 +335,26 @@ namespace Barotrauma { Prefab = prefab.Prefab ?? prefab; - Name = prefab.Name; - Identifier = prefab.Identifier; - ItemComponentType = prefab.ItemComponentType; - CanTypeBeSubclass = prefab.CanTypeBeSubclass; - TargetItems = prefab.TargetItems; - Options = prefab.Options; - SymbolSprite = prefab.SymbolSprite; - Color = prefab.Color; - UseController = prefab.UseController; - TargetAllCharacters = prefab.TargetAllCharacters; - AppropriateJobs = prefab.AppropriateJobs; - FadeOutTime = prefab.FadeOutTime; - MustSetTarget = prefab.MustSetTarget; - AppropriateSkill = prefab.AppropriateSkill; - Category = prefab.Category; - MustManuallyAssign = prefab.MustManuallyAssign; - IsIgnoreOrder = prefab.IsIgnoreOrder; + Name = prefab.Name; + ContextualName = prefab.ContextualName; + Identifier = prefab.Identifier; + ItemComponentType = prefab.ItemComponentType; + CanTypeBeSubclass = prefab.CanTypeBeSubclass; + TargetItems = prefab.TargetItems; + Options = prefab.Options; + SymbolSprite = prefab.SymbolSprite; + Color = prefab.Color; + UseController = prefab.UseController; + TargetAllCharacters = prefab.TargetAllCharacters; + AppropriateJobs = prefab.AppropriateJobs; + FadeOutTime = prefab.FadeOutTime; + MustSetTarget = prefab.MustSetTarget; + AppropriateSkill = prefab.AppropriateSkill; + Category = prefab.Category; + MustManuallyAssign = prefab.MustManuallyAssign; + IsIgnoreOrder = prefab.IsIgnoreOrder; + DrawIconWhenContained = prefab.DrawIconWhenContained; + Hidden = prefab.Hidden; OrderGiver = orderGiver; TargetEntity = targetEntity; @@ -351,9 +365,7 @@ namespace Barotrauma ConnectedController = targetItem.Item?.FindController(); if (ConnectedController == null) { -#if DEBUG - throw new Exception("Tried to use controller, but couldn't find one"); -#endif + DebugConsole.AddWarning("AI: Tried to use a controller for operating an item, but couldn't find any."); UseController = false; } } @@ -433,7 +445,8 @@ namespace Barotrauma return firstMatchingComponent != null; } - public List GetMatchingItems(Submarine submarine, bool mustBelongToPlayerSub, Character.TeamType? requiredTeam = null) + /// Only returns items which are interactable for this character + public List GetMatchingItems(Submarine submarine, bool mustBelongToPlayerSub, CharacterTeamType? requiredTeam = null, Character interactableFor = null) { List matchingItems = new List(); if (submarine == null) { return matchingItems; } @@ -456,16 +469,23 @@ namespace Barotrauma { 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))); + } } return matchingItems; } - public List GetMatchingItems(bool mustBelongToPlayerSub) + + /// Only returns items which are interactable for this character + public List GetMatchingItems(bool mustBelongToPlayerSub, Character interactableFor = null) { - Submarine submarine = Character.Controlled != null && Character.Controlled.TeamID == Character.TeamType.Team2 && Submarine.MainSubs.Length > 1 ? + Submarine submarine = Character.Controlled != null && Character.Controlled.TeamID == CharacterTeamType.Team2 && Submarine.MainSubs.Length > 1 ? Submarine.MainSubs[1] : Submarine.MainSub; - return GetMatchingItems(submarine, mustBelongToPlayerSub); + return GetMatchingItems(submarine, mustBelongToPlayerSub, interactableFor: interactableFor); } public string GetOptionName(string id) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs index 90c20a4f9..5c653e32d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs @@ -254,11 +254,8 @@ namespace Barotrauma { if (AiController.Character.Inventory != null) { - var items = AiController.Character.Inventory.Items; - for (int i = 0; i < items.Length; i++) + foreach (Item item in AiController.Character.Inventory.AllItems) { - var item = items[i]; - if (item == null) { continue; } var tag = item.GetComponent(); if (tag != null && !string.IsNullOrWhiteSpace(tag.WrittenName)) { @@ -358,7 +355,7 @@ namespace Barotrauma XElement petElement = new XElement("pet", new XAttribute("speciesname", c.SpeciesName), - new XAttribute("ownerid", petBehavior.Owner?.ID ?? Entity.NullEntityID), + new XAttribute("ownerhash", petBehavior.Owner?.Info?.GetIdentifier() ?? 0), new XAttribute("seed", c.Seed)); var petBehaviorElement = new XElement("petbehavior", @@ -387,16 +384,19 @@ namespace Barotrauma { string speciesName = subElement.GetAttributeString("speciesname", ""); string seed = subElement.GetAttributeString("seed", "123"); - ushort ownerID = (ushort)subElement.GetAttributeInt("ownerid", 0); + int ownerHash = subElement.GetAttributeInt("ownerhash", 0); Vector2 spawnPos = Vector2.Zero; - Character owner = Entity.FindEntityByID(ownerID) as Character; - if (owner != null) + Character owner = Character.CharacterList.Find(c => c.Info?.GetIdentifier() == ownerHash); + if (owner != null && owner.Submarine?.Info.Type == SubmarineType.Player) { spawnPos = owner.WorldPosition; } else { - var spawnPoint = WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine?.Info.Type == SubmarineType.Player).GetRandom(); + //try to find a spawnpoint in the main sub + var spawnPoint = WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine == Submarine.MainSub).GetRandom(); + //if not found, try any player sub (shuttle/drone etc) + spawnPoint ??= WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine?.Info.Type == SubmarineType.Player).GetRandom(); spawnPos = spawnPoint?.WorldPosition ?? Submarine.MainSub.WorldPosition; } var pet = Character.Create(speciesName, spawnPos, seed); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs index 69976c41d..4ec8d6ee6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs @@ -131,7 +131,7 @@ namespace Barotrauma if (container == null) { continue; } for (int i = 0; i < container.Inventory.Capacity; i++) { - if (container.Inventory.Items[i] != null) { continue; } + if (container.Inventory.GetItemAt(i) != null) { continue; } if (MapEntityPrefab.List.GetRandom(e => e is ItemPrefab i && container.CanBeContained(i) && Config.ForbiddenAmmunition.None(id => id.Equals(i.Identifier, StringComparison.OrdinalIgnoreCase)), Rand.RandSync.Server) is ItemPrefab ammoPrefab) { @@ -319,25 +319,33 @@ namespace Barotrauma private readonly List populatedHulls = new List(); private float cellSpawnTimer; - private float CellSpawnTime => Config.AgentSpawnDelay; - private float CellSpawnRandomFactor => Config.AgentSpawnDelayRandomFactor; - private int MinCellsPerBrainRoom => Config.MinAgentsPerBrainRoom; - private int MaxCellsPerRoom => Config.MaxAgentsPerRoom; - private int MinCellsOutside => Config.MinAgentsOutside; - private int MaxCellsOutside => Config.MaxAgentsOutside; - private int MinCellsInside => Config.MinAgentsInside; - private int MaxCellsInside => Config.MaxAgentsInside; - private int MaxCellCount => Config.MaxAgentCount; + private int MinCellsPerBrainRoom => CalculateCellCount(0, Config.MinAgentsPerBrainRoom); + private int MaxCellsPerRoom => CalculateCellCount(1, Config.MaxAgentsPerRoom); + private int MinCellsOutside => CalculateCellCount(0, Config.MinAgentsOutside); + private int MaxCellsOutside => CalculateCellCount(0, Config.MaxAgentsOutside); + private int MinCellsInside => CalculateCellCount(2, Config.MinAgentsInside); + private int MaxCellsInside => CalculateCellCount(3, Config.MaxAgentsInside); + private int MaxCellCount => CalculateCellCount(5, Config.MaxAgentCount); private float MinWaterLevel => Config.MinWaterLevel; + private int CalculateCellCount(int minValue, int maxValue) + { + if (maxValue == 0) { return 0; } + return (int)Math.Round(MathHelper.Lerp(minValue, maxValue, Level.Loaded.Difficulty * 0.01f * Config.AgentSpawnCountDifficultyMultiplier)); + } + + private float GetSpawnTime() => + Math.Max(Config.AgentSpawnDelay * Rand.Range(Config.AgentSpawnDelayRandomFactor, 1 + Config.AgentSpawnDelayRandomFactor) + / (Math.Max(Level.Loaded.Difficulty, 1) * 0.01f * Config.AgentSpawnDelayDifficultyMultiplier), Config.AgentSpawnDelay); + void UpdateReinforcements(float deltaTime) { - if (protectiveCells.Count >= MaxCellCount || spawnOrgans.Count == 0) { return; } + if (spawnOrgans.Count == 0) { return; } cellSpawnTimer -= deltaTime; if (cellSpawnTimer < 0) { TrySpawnCell(out _, spawnOrgans.GetRandom()); - cellSpawnTimer = CellSpawnTime * Rand.Range(CellSpawnRandomFactor, 1 + CellSpawnRandomFactor); + cellSpawnTimer = GetSpawnTime(); } } @@ -364,7 +372,7 @@ namespace Barotrauma cell = Character.Create(Config.DefensiveAgent, targetEntity.WorldPosition, ToolBox.RandomSeed(8), hasAi: true, createNetworkEvent: true); protectiveCells.Add(cell); cell.OnDeath += OnCellDeath; - cellSpawnTimer = CellSpawnTime * Rand.Range(CellSpawnRandomFactor, 1 + CellSpawnRandomFactor); + cellSpawnTimer = GetSpawnTime(); return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAIConfig.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAIConfig.cs index 9830a8d4d..e2a9c9b0a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAIConfig.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAIConfig.cs @@ -42,6 +42,12 @@ namespace Barotrauma [Serialize(0.5f, false)] public float AgentSpawnDelayRandomFactor { get; private set; } + [Serialize(1f, false)] + public float AgentSpawnDelayDifficultyMultiplier { get; private set; } + + [Serialize(1f, false)] + public float AgentSpawnCountDifficultyMultiplier { get; private set; } + [Serialize(0, false)] public int MinAgentsPerBrainRoom { get; private set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs index d934f5822..98b7f762f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs @@ -20,8 +20,8 @@ namespace Barotrauma get { return aiController; } } - public AICharacter(string speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, bool isNetworkPlayer = false, RagdollParams ragdoll = null) - : base(speciesName, position, seed, characterInfo, id: Entity.NullEntityID, isRemotePlayer: isNetworkPlayer, ragdollParams: ragdoll) + public AICharacter(CharacterPrefab prefab, string speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, bool isNetworkPlayer = false, RagdollParams ragdoll = null) + : base(prefab, speciesName, position, seed, characterInfo, id: Entity.NullEntityID, isRemotePlayer: isNetworkPlayer, ragdollParams: ragdoll) { InitProjSpecific(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs index b08df6dab..cf8585f14 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs @@ -1,6 +1,5 @@ using Barotrauma.Networking; using FarseerPhysics; -using FarseerPhysics.Dynamics.Joints; using Microsoft.Xna.Framework; using System; using System.Linq; @@ -23,7 +22,11 @@ namespace Barotrauma { if (_ragdollParams == null) { - _ragdollParams = FishRagdollParams.GetDefaultRagdollParams(character.SpeciesName); + _ragdollParams = FishRagdollParams.GetDefaultRagdollParams(character.VariantOf ?? character.SpeciesName); + if (character.VariantOf != null) + { + _ragdollParams.ApplyVariantScale(character.Params.VariantFile); + } } return _ragdollParams; } @@ -338,9 +341,21 @@ namespace Barotrauma float dragForce = MathHelper.Clamp(eatSpeed * 10, 0, 40); if (dragForce > 0.1f) { - target.AnimController.MainLimb.MoveToPos(mouthPos, (float)(Math.Sin(eatTimer) + dragForce)); + Vector2 targetPos = mouthPos; + if (target.Submarine != null && character.Submarine == null) + { + targetPos -= target.Submarine.SimPosition; + } + else if (target.Submarine == null && character.Submarine != null) + { + targetPos += character.Submarine.SimPosition; + } target.AnimController.MainLimb.body.SmoothRotate(mouthLimb.Rotation, dragForce * 2); - target.AnimController.Collider.MoveToPos(mouthPos, (float)(Math.Sin(eatTimer) + dragForce)); + if (!target.AnimController.SimplePhysicsEnabled) + { + target.AnimController.MainLimb.MoveToPos(targetPos, (float)(Math.Sin(eatTimer) + dragForce)); + } + target.AnimController.Collider.MoveToPos(targetPos, (float)(Math.Sin(eatTimer) + dragForce)); } if (InWater) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index e811835b8..747e56813 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -26,7 +26,7 @@ namespace Barotrauma { if (_ragdollParams == null) { - _ragdollParams = RagdollParams.GetDefaultRagdollParams(character.SpeciesName); + _ragdollParams = RagdollParams.GetDefaultRagdollParams(character.VariantOf ?? character.SpeciesName); } return _ragdollParams; } @@ -201,6 +201,8 @@ namespace Barotrauma public float LegBendTorque => CurrentGroundedParams.LegBendTorque * RagdollParams.JointScale; public Vector2 HandMoveOffset => CurrentGroundedParams.HandMoveOffset * RagdollParams.JointScale; + public float LockFlippingUntil; + public override Vector2 AimSourceSimPos { get @@ -518,7 +520,7 @@ namespace Barotrauma break; } - if (TargetDir != dir && !IsStuck) + if (Timing.TotalTime > LockFlippingUntil && TargetDir != dir && !IsStuck) { Flip(); } @@ -1459,7 +1461,7 @@ namespace Barotrauma target.CharacterHealth.CalculateVitality(); if (wasCritical && target.Vitality > 0.0f && Timing.TotalTime > lastReviveTime + 10.0f) { - character.Info.IncreaseSkillLevel("medical", SkillSettings.Current.SkillIncreasePerCprRevive, character.WorldPosition + Vector2.UnitY * 150.0f); + character.Info.IncreaseSkillLevel("medical", SkillSettings.Current.SkillIncreasePerCprRevive, character.Position + Vector2.UnitY * 150.0f); SteamAchievementManager.OnCharacterRevived(target, character); lastReviveTime = (float)Timing.TotalTime; #if SERVER @@ -1467,7 +1469,7 @@ namespace Barotrauma #endif //reset attacker, we don't want the character to start attacking us //because we caused a bit of damage to them during CPR - if (target.LastAttacker == character) { target.LastAttacker = null; } + target.ForgiveAttacker(character); } } } @@ -1771,13 +1773,13 @@ namespace Barotrauma Vector2 transformedHoldPos = rightShoulder.WorldAnchorA; if (itemPos == Vector2.Zero || isClimbing || usingController) { - if (character.SelectedItems[0] == item) + if (character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand) == item) { if (rightHand == null || rightHand.IsSevered) { return; } transformedHoldPos = rightHand.PullJointWorldAnchorA - transformedHandlePos[0]; itemAngle = (rightHand.Rotation + (holdAngle - MathHelper.PiOver2) * Dir); } - else if (character.SelectedItems[1] == item) + else if (character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand) == item) { if (leftHand == null || leftHand.IsSevered) { return; } transformedHoldPos = leftHand.PullJointWorldAnchorA - transformedHandlePos[1]; @@ -1786,13 +1788,13 @@ namespace Barotrauma } else { - if (character.SelectedItems[0] == item) + if (character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand) == item) { if (rightHand == null || rightHand.IsSevered) { return; } transformedHoldPos = rightShoulder.WorldAnchorA; rightHand.Disabled = true; } - if (character.SelectedItems[1] == item) + if (character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand) == item) { if (leftHand == null || leftHand.IsSevered) { return; } transformedHoldPos = leftShoulder.WorldAnchorA; @@ -1805,7 +1807,7 @@ namespace Barotrauma item.body.ResetDynamics(); - Vector2 currItemPos = (character.SelectedItems[0] == item) ? + Vector2 currItemPos = (character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand) == item) ? rightHand.PullJointWorldAnchorA - transformedHandlePos[0] : leftHand.PullJointWorldAnchorA - transformedHandlePos[1]; @@ -1853,15 +1855,14 @@ namespace Barotrauma } } - item.SetTransform(currItemPos, itemAngle + itemAngleRelativeToHoldAngle * Dir, setPrevTransform: false); + item.SetTransform(currItemPos, itemAngle + itemAngleRelativeToHoldAngle * Dir, setPrevTransform: false); - if (!isClimbing && !character.IsIncapacitated) + if (!isClimbing && !character.IsIncapacitated && itemPos != Vector2.Zero) { for (int i = 0; i < 2; i++) { - if (character.SelectedItems[i] != item || itemPos == Vector2.Zero) { continue; } - Limb hand = (i == 0) ? rightHand : leftHand; - HandIK(hand, transformedHoldPos + transformedHandlePos[i]); + if (!character.Inventory.IsInLimbSlot(item, i == 0 ? InvSlotType.RightHand : InvSlotType.LeftHand)) { continue; } + HandIK(i == 0 ? rightHand : leftHand, transformedHoldPos + transformedHandlePos[i]); } } } @@ -2032,16 +2033,13 @@ namespace Barotrauma Matrix torsoTransform = Matrix.CreateRotationZ(torso.Rotation); - for (int i = 0; i < character.SelectedItems.Length; i++) + foreach (Item heldItem in character.HeldItems) { - if (i == 1 && character.SelectedItems[0] == character.SelectedItems[1]) + if (heldItem?.body != null && !heldItem.Removed && heldItem.GetComponent() != null) { - break; - } - if (character.SelectedItems[i]?.body != null && !character.SelectedItems[i].Removed && character.SelectedItems[i].GetComponent() != null) - { - character.SelectedItems[i].FlipX(relativeToSub: false); + heldItem.FlipX(relativeToSub: false); } + heldItem.FlipX(relativeToSub: false); } foreach (Limb limb in Limbs) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index c3ea7e533..b601d6b30 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -758,11 +758,11 @@ namespace Barotrauma limb.IsSevered = true; if (limb.type == LimbType.RightHand) { - character.SelectedItems[0]?.Drop(character); + character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand)?.Drop(character); } else if (limb.type == LimbType.LeftHand) { - character.SelectedItems[1]?.Drop(character); + character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand)?.Drop(character); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index 91af8d0a2..0271f0f03 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -121,11 +121,26 @@ namespace Barotrauma [Serialize(false, true), Editable] public bool FullSpeedAfterAttack { get; private set; } + private float _structureDamage; [Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)] - public float StructureDamage { get; set; } + public float StructureDamage + { + get => _structureDamage * DamageMultiplier; + set => _structureDamage = value; + } + private float _itemDamage; [Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] - public float ItemDamage { get; set; } + public float ItemDamage + { + get =>_itemDamage * DamageMultiplier; + set => _itemDamage = value; + } + + /// + /// Currently only used with variants. Used for multiplying all the damage. + /// + public float DamageMultiplier { get; set; } = 1; [Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] public float LevelWallDamage { get; set; } @@ -280,7 +295,7 @@ namespace Barotrauma { totalDamage += affliction.GetVitalityDecrease(null); } - return totalDamage; + return totalDamage * DamageMultiplier; } public Attack(float damage, float bleedingDamage, float burnDamage, float structureDamage, float itemDamage, float range = 0.0f) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 017ff62d0..9b2ad9925 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -1,6 +1,5 @@ using Barotrauma.Networking; using FarseerPhysics; -using FarseerPhysics.Dynamics.Joints; using Microsoft.Xna.Framework; using System; using Barotrauma.IO; @@ -16,6 +15,14 @@ using System.Text; namespace Barotrauma { + public enum CharacterTeamType + { + None = 0, + Team1 = 1, + Team2 = 2, + FriendlyNPC = 3 + } + partial class Character : Entity, IDamageable, ISerializableEntity, IClientSerializable, IServerSerializable { public static List CharacterList = new List(); @@ -101,18 +108,9 @@ namespace Barotrauma } protected Key[] keys; - private readonly Item[] selectedItems; - public enum TeamType - { - None, - Team1, - Team2, - FriendlyNPC - } - - private TeamType teamID; - public TeamType TeamID + private CharacterTeamType teamID; + public CharacterTeamType TeamID { get { return teamID; } set @@ -122,6 +120,8 @@ namespace Barotrauma } } + public bool IsOnPlayerTeam => TeamID == CharacterTeamType.Team1 || TeamID == CharacterTeamType.Team2; + public bool IsInstigator => CombatAction != null && CombatAction.IsInstigator; public CombatAction CombatAction; @@ -135,7 +135,25 @@ namespace Barotrauma public readonly string Seed; protected Item focusedItem; private Character selectedCharacter, selectedBy; - public Character LastAttacker; + + private const int maxLastAttackerCount = 4; + + public class Attacker + { + public Character Character; + public float Damage; + } + + private readonly List lastAttackers = new List(); + public IEnumerable LastAttackers + { + get { return lastAttackers; } + } + public Character LastAttacker + { + get { return lastAttackers.Count > 0 ? lastAttackers[lastAttackers.Count - 1].Character : null; } + } + public Entity LastDamageSource; public float InvisibleTimer; @@ -260,6 +278,8 @@ namespace Barotrauma } } + public string VariantOf { get; private set; } + public string Name { get @@ -429,6 +449,20 @@ namespace Barotrauma } } + /// + /// Items the character has in their hand slots. Doesn't return nulls and only returns items held in both hands once. + /// + public IEnumerable HeldItems + { + get + { + var item1 = Inventory?.GetItemInLimbSlot(InvSlotType.RightHand); + var item2 = Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand); + if (item1 != null) { yield return item1; } + if (item2 != null && item2 != item1) { yield return item2; } + } + } + private float lowPassMultiplier; public float LowPassMultiplier { @@ -445,7 +479,7 @@ namespace Barotrauma } set { - obstructVisionAmount = 1.0f; + obstructVisionAmount = value ? 1.0f : 0.0f; } } @@ -498,6 +532,8 @@ namespace Barotrauma get { return oxygenAvailable; } set { oxygenAvailable = MathHelper.Clamp(value, 0.0f, 100.0f); } } + + public bool UseHullOxygen { get; set; } = true; public float Stun { @@ -575,9 +611,12 @@ namespace Barotrauma set; } - public Item[] SelectedItems + /// + /// Current speed of the character's collider. Can be used by status effects to check if the character is moving. + /// + public float CurrentSpeed { - get { return selectedItems; } + get { return AnimController?.Collider?.LinearVelocity.Length() ?? 0.0f; } } private Item _selectedConstruction; @@ -620,7 +659,23 @@ namespace Barotrauma get { return null; } } - public bool IsDead { get; private set; } + private bool isDead; + public bool IsDead + { + get { return isDead; } + set + { + if (isDead == value) { return; } + if (value) + { + Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null); + } + else + { + Revive(); + } + } + } public bool IsObserving => AIController is EnemyAIController enemyAI && enemyAI.Enabled && enemyAI.State == AIState.Observe; @@ -666,7 +721,7 @@ namespace Barotrauma } else { - return (IsDead || Stun > 0.0f || LockHands || IsIncapacitated); + return IsDead || Stun > 0.0f || LockHands || IsIncapacitated; } } set { canInventoryBeAccessed = value; } @@ -680,6 +735,8 @@ namespace Barotrauma } } + public bool InWater => AnimController?.InWater ?? false; + public bool GodMode = false; public CampaignMode.InteractionType CampaignInteractionType; @@ -770,7 +827,8 @@ namespace Barotrauma speciesName = Path.GetFileNameWithoutExtension(speciesName).ToLowerInvariant(); } - if (CharacterPrefab.FindBySpeciesName(speciesName) == null) + var prefab = CharacterPrefab.FindBySpeciesName(speciesName); + if (prefab == null) { DebugConsole.ThrowError($"Failed to create character \"{speciesName}\". Matching prefab not found.\n" + Environment.StackTrace); return null; @@ -779,21 +837,21 @@ namespace Barotrauma Character newCharacter = null; if (!speciesName.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase)) { - var aiCharacter = new AICharacter(speciesName, position, seed, characterInfo, isRemotePlayer, ragdoll); + var aiCharacter = new AICharacter(prefab, speciesName, position, seed, characterInfo, isRemotePlayer, ragdoll); var ai = new EnemyAIController(aiCharacter, seed); aiCharacter.SetAI(ai); newCharacter = aiCharacter; } else if (hasAi) { - var aiCharacter = new AICharacter(speciesName, position, seed, characterInfo, isRemotePlayer, ragdoll); + var aiCharacter = new AICharacter(prefab, speciesName, position, seed, characterInfo, isRemotePlayer, ragdoll); var ai = new HumanAIController(aiCharacter); aiCharacter.SetAI(ai); newCharacter = aiCharacter; } else { - newCharacter = new Character(speciesName, position, seed, characterInfo, id: id, isRemotePlayer: isRemotePlayer, ragdollParams: ragdoll); + newCharacter = new Character(prefab, speciesName, position, seed, characterInfo, id: id, isRemotePlayer: isRemotePlayer, ragdollParams: ragdoll); } float healthRegen = newCharacter.Params.Health.ConstantHealthRegeneration; @@ -833,16 +891,14 @@ namespace Barotrauma return newCharacter; } - protected Character(string speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, RagdollParams ragdollParams = null) + protected Character(CharacterPrefab prefab, string speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, RagdollParams ragdollParams = null) : base(null, id) { - prefab = CharacterPrefab.FindBySpeciesName(speciesName); - + VariantOf = prefab.VariantOf; this.Seed = seed; + this.prefab = prefab; MTRandom random = new MTRandom(ToolBox.StringToInt(seed)); - selectedItems = new Item[2]; - IsRemotePlayer = isRemotePlayer; oxygenAvailable = 100.0f; @@ -851,11 +907,19 @@ namespace Barotrauma lowPassMultiplier = 1.0f; Properties = SerializableProperty.GetProperties(this); + Params = new CharacterParams(prefab.FilePath); Info = characterInfo; + + speciesName = VariantOf ?? speciesName; + if (speciesName.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase)) { + if (VariantOf != null) + { + DebugConsole.ThrowError("The variant system does not yet support humans, sorry. It does support other humanoids though!"); + } if (characterInfo == null) { Info = new CharacterInfo(CharacterPrefab.HumanSpeciesName); @@ -873,6 +937,10 @@ namespace Barotrauma } var rootElement = prefab.XDocument.Root; + if (VariantOf != null) + { + rootElement = CharacterPrefab.FindBySpeciesName(VariantOf)?.XDocument?.Root; + } var mainElement = rootElement.IsOverride() ? rootElement.FirstElement() : rootElement; InitProjSpecific(mainElement); @@ -897,6 +965,36 @@ namespace Barotrauma break; } } + if (Params.VariantFile != null) + { + XElement overrideElement = Params.VariantFile.Root; + // Only override if the override file contains matching elements + if (overrideElement.GetChildElement("inventory") != null) + { + inventoryElements.Clear(); + inventoryCommonness.Clear(); + foreach (XElement subElement in overrideElement.GetChildElements("inventory")) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "inventory": + inventoryElements.Add(subElement); + inventoryCommonness.Add(subElement.GetAttributeFloat("commonness", 1.0f)); + break; + } + } + } + if (overrideElement.GetChildElement("health") != null) + { + healthElements.Clear(); + healthCommonness.Clear(); + foreach (XElement subElement in overrideElement.GetChildElements("health")) + { + healthElements.Add(subElement); + healthCommonness.Add(subElement.GetAttributeFloat("commonness", 1.0f)); + } + } + } if (inventoryElements.Count > 0) { @@ -910,9 +1008,14 @@ namespace Barotrauma } else { - CharacterHealth = new CharacterHealth( - healthElements.Count == 1 ? healthElements[0] : ToolBox.SelectWeightedRandom(healthElements, healthCommonness, random), - this); + var selectedHealthElement = healthElements.Count == 1 ? healthElements[0] : ToolBox.SelectWeightedRandom(healthElements, healthCommonness, random); + // If there's no limb elements defined in the override variant, let's use the limb health definitions of the original file. + var limbHealthElement = selectedHealthElement; + if (Params.VariantFile != null && limbHealthElement.GetChildElement("limb") == null) + { + limbHealthElement = Params.OriginalElement.GetChildElement("health"); + } + CharacterHealth = new CharacterHealth(selectedHealthElement, this, limbHealthElement); } if (Params.Husk) @@ -928,6 +1031,7 @@ namespace Barotrauma DebugConsole.ThrowError("Cannot find a husk infection that matches this species! Please add the speciesnames as 'targets' in the husk affliction prefab definition!"); // Crashes if we fail to create a ragdoll -> Let's just use some ragdoll so that the user sees the error msg. nonHuskedSpeciesName = IsHumanoid ? CharacterPrefab.HumanSpeciesName : "crawler"; + speciesName = nonHuskedSpeciesName; } else { @@ -1161,7 +1265,7 @@ namespace Barotrauma { if (info?.Job == null || spawnPoint == null) { return; } - foreach (Item item in Inventory.Items) + foreach (Item item in Inventory.AllItems) { if (item?.Prefab.Identifier != "idcard") { continue; } foreach (string s in spawnPoint.IdCardTags) @@ -1558,12 +1662,8 @@ namespace Barotrauma if (SelectedConstruction == null || !SelectedConstruction.Prefab.DisableItemUsageWhenSelected) { - for (int i = 0; i < selectedItems.Length; i++) + foreach (Item item in HeldItems) { - if (selectedItems[i] == null) { continue; } - if (i == 1 && selectedItems[0] == selectedItems[1]) { continue; } - var item = selectedItems[i]; - if (item == null) { continue; } if (IsKeyDown(InputType.Aim) || !item.RequireAimToSecondaryUse) { item.SecondaryUse(deltaTime, this); @@ -1712,7 +1812,7 @@ namespace Barotrauma var door = item.GetComponent(); if (door != null) { - return !door.IsOpen && !door.IsBroken; + return !door.CanBeTraversed; } } return false; @@ -1739,7 +1839,7 @@ namespace Barotrauma Structure wall = closestBody.UserData as Structure; Item item = closestBody.UserData as Item; Door door = item?.GetComponent(); - return (wall == null || !wall.CastShadow) && (door == null || door.IsOpen || door.IsBroken); + return (wall == null || !wall.CastShadow) && (door == null || door.CanBeTraversed); } /// @@ -1754,9 +1854,8 @@ namespace Barotrauma if (Inventory == null) { return false; } for (int i = 0; i < Inventory.Capacity; i++) { - if (Inventory.Items[i] == item && Inventory.SlotTypes[i] != InvSlotType.Any) { return true; } + if (Inventory.SlotTypes[i] != InvSlotType.Any && Inventory.GetItemAt(i) == item) { return true; } } - return false; } @@ -1765,55 +1864,15 @@ namespace Barotrauma if (Inventory == null) { return false; } for (int i = 0; i < Inventory.Capacity; i++) { - if (Inventory.SlotTypes[i] == InvSlotType.Any || Inventory.Items[i] == null) { continue; } - if (!allowBroken && Inventory.Items[i].Condition <= 0.0f) { continue; } - if (Inventory.Items[i].Prefab.Identifier == tagOrIdentifier || Inventory.Items[i].HasTag(tagOrIdentifier)) { return true; } + if (Inventory.SlotTypes[i] == InvSlotType.Any) { continue; } + var item = Inventory.GetItemAt(i); + if (item == null) { continue; } + if (!allowBroken && item.Condition <= 0.0f) { continue; } + if (item.Prefab.Identifier == tagOrIdentifier || item.HasTag(tagOrIdentifier)) { return true; } } - return false; } - public bool HasSelectedItem(Item item) - { - return selectedItems.Contains(item); - } - - public bool TrySelectItem(Item item) - { - bool rightHand = Inventory.IsInLimbSlot(item, InvSlotType.RightHand); - bool leftHand = Inventory.IsInLimbSlot(item, InvSlotType.LeftHand); - - bool selected = false; - if (rightHand && (selectedItems[0] == null || selectedItems[0] == item)) - { - selectedItems[0] = item; - selected = true; - } - if (leftHand && (selectedItems[1] == null || selectedItems[1] == item)) - { - selectedItems[1] = item; - selected = true; - } - - return selected; - } - - public bool TrySelectItem(Item item, int index) - { - if (selectedItems[index] != null) { return false; } - - selectedItems[index] = item; - return true; - } - - public void DeselectItem(Item item) - { - for (int i = 0; i < selectedItems.Length; i++) - { - if (selectedItems[i] == item) selectedItems[i] = null; - } - } - public bool CanAccessInventory(Inventory inventory) { if (!CanInteract || inventory.Locked) { return false; } @@ -1848,7 +1907,7 @@ namespace Barotrauma /// public bool FindItem(ref int itemIndex, out Item targetItem, IEnumerable identifiers = null, bool ignoreBroken = true, IEnumerable ignoredItems = null, IEnumerable ignoredContainerIdentifiers = null, - Func customPredicate = null, Func customPriorityFunction = null, float maxItemDistance = 10000) + Func customPredicate = null, Func customPriorityFunction = null, float maxItemDistance = 10000, ISpatialEntity positionalReference = null) { if (itemIndex == 0) { @@ -1859,7 +1918,7 @@ namespace Barotrauma { itemIndex++; var item = Item.ItemList[itemIndex]; - if (item.NonInteractable) { continue; } + if (!item.IsInteractable(this)) { continue; } if (ignoredItems != null && ignoredItems.Contains(item)) { continue; } if (item.Submarine == null) { continue; } if (item.Submarine.TeamID != TeamID) { continue; } @@ -1879,10 +1938,15 @@ namespace Barotrauma float itemPriority = customPriorityFunction != null ? customPriorityFunction(item) : 1; if (itemPriority <= 0) { continue; } Entity rootInventoryOwner = item.GetRootInventoryOwner(); + if (rootInventoryOwner is Item ownerItem) + { + if (!ownerItem.IsInteractable(this)) { continue; } + } Vector2 itemPos = (rootInventoryOwner ?? item).WorldPosition; - float yDist = Math.Abs(WorldPosition.Y - itemPos.Y); + Vector2 refPos = positionalReference != null ? positionalReference.WorldPosition : WorldPosition; + float yDist = Math.Abs(refPos.Y - itemPos.Y); yDist = yDist > 100 ? yDist * 5 : 0; - float dist = Math.Abs(WorldPosition.X - itemPos.X) + yDist; + float dist = Math.Abs(refPos.X - itemPos.X) + yDist; float distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, maxItemDistance, dist)); itemPriority *= distanceFactor; if (itemPriority > _selectedItemPriority) @@ -1924,7 +1988,7 @@ namespace Barotrauma #if CLIENT if (Screen.Selected == GameMain.SubEditorScreen) { hidden = false; } #endif - if (!CanInteract || hidden || item.NonInteractable) { return false; } + if (!CanInteract || hidden || !item.IsInteractable(this)) { return false; } if (item.ParentInventory != null) { @@ -2115,7 +2179,7 @@ namespace Barotrauma FocusedCharacter = CanInteract ? FindCharacterAtPosition(mouseSimPos) : null; if (FocusedCharacter != null && !CanSeeCharacter(FocusedCharacter)) { FocusedCharacter = null; } float aimAssist = GameMain.Config.AimAssistAmount * (AnimController.InWater ? 1.5f : 1.0f); - if (SelectedItems.Any(it => it?.GetComponent()?.IsActive ?? false)) + if (HeldItems.Any(it => it?.GetComponent()?.IsActive ?? false)) { //disable aim assist when rewiring to make it harder to accidentally select items when adding wire nodes aimAssist = 0.0f; @@ -2350,10 +2414,9 @@ namespace Barotrauma if (Inventory != null) { - foreach (Item item in Inventory.Items) + foreach (Item item in Inventory.AllItems) { - if (item == null || item.body == null || item.body.Enabled) { continue; } - + if (item.body == null || item.body.Enabled) { continue; } item.SetTransform(SimPosition, 0.0f); item.Submarine = Submarine; } @@ -2361,10 +2424,11 @@ namespace Barotrauma HideFace = false; - UpdateSightRange(deltaTime); UpdateSoundRange(deltaTime); + UpdateAttackers(deltaTime); + if (IsDead) { return; } if (GameMain.NetworkMember != null) @@ -2523,6 +2587,56 @@ namespace Barotrauma partial void SetOrderProjSpecific(Order order, string orderOption); + + public void AddAttacker(Character character, float damage) + { + Attacker attacker = lastAttackers.FirstOrDefault(a => a.Character == character); + if (attacker != null) + { + lastAttackers.Remove(attacker); + } + else + { + attacker = new Attacker { Character = character }; + } + + if (lastAttackers.Count > maxLastAttackerCount) + { + lastAttackers.RemoveRange(0, lastAttackers.Count - maxLastAttackerCount); + } + + attacker.Damage += damage; + lastAttackers.Add(attacker); + } + + public void ForgiveAttacker(Character character) + { + int index; + if ((index = lastAttackers.FindIndex(a => a.Character == character)) >= 0) + { + lastAttackers.RemoveAt(index); + } + } + + private void UpdateAttackers(float deltaTime) + { + //slowly forget about damage done by attackers + foreach (Attacker enemy in LastAttackers) + { + float cumulativeDamage = enemy.Damage; + if (cumulativeDamage > 0) + { + float reduction = deltaTime; + if (cumulativeDamage < 2) + { + // If the damage is very low, let's not forget so quickly, or we can't cumulate the damage from repair tools (high frequency, low damage) + reduction *= 0.5f; + } + enemy.Damage = Math.Max(0.0f, enemy.Damage-reduction); + } + } + } + private void UpdateOxygen(float deltaTime) { if (NeedsAir) @@ -2545,7 +2659,7 @@ namespace Barotrauma { //don't decrease the amount of oxygen in the hull if the character has more oxygen available than the hull //(i.e. if the character has some external source of oxygen) - if (OxygenAvailable * 0.98f < AnimController.CurrentHull.OxygenPercentage) + if (OxygenAvailable * 0.98f < AnimController.CurrentHull.OxygenPercentage && UseHullOxygen) { AnimController.CurrentHull.Oxygen -= Hull.OxygenConsumptionSpeed * deltaTime; } @@ -2554,6 +2668,7 @@ namespace Barotrauma } OxygenAvailable += MathHelper.Clamp(hullAvailableOxygen - oxygenAvailable, -deltaTime * 50.0f, deltaTime * 50.0f); } + UseHullOxygen = true; } /// @@ -2661,18 +2776,16 @@ namespace Barotrauma void onItemContainerSpawned(Item item) { - if (Inventory?.Items == null) { return; } + if (Inventory == null) { return; } - item.UpdateTransform(); - + item.UpdateTransform(); item.AddTag("name:" + Name); if (info?.Job != null) { item.AddTag("job:" + info.Job.Name); } var itemContainer = item?.GetComponent(); if (itemContainer == null) { return; } - foreach (Item inventoryItem in Inventory.Items) + foreach (Item inventoryItem in Inventory.AllItemsMod) { - if (inventoryItem == null) { continue; } if (!itemContainer.Inventory.TryPutItem(inventoryItem, user: null)) { //if the item couldn't be put inside the despawn container, just drop it @@ -2907,8 +3020,8 @@ namespace Barotrauma float attackImpulse = attack.TargetImpulse + attack.TargetForce * deltaTime; var attackResult = targetLimb == null ? - AddDamage(worldPosition, attack.Afflictions.Keys, attack.Stun, playSound, attackImpulse, out limbHit, attacker) : - DamageLimb(worldPosition, targetLimb, attack.Afflictions.Keys, attack.Stun, playSound, attackImpulse, attacker); + AddDamage(worldPosition, attack.Afflictions.Keys, attack.Stun, playSound, attackImpulse, out limbHit, attacker, attack.DamageMultiplier) : + DamageLimb(worldPosition, targetLimb, attack.Afflictions.Keys, attack.Stun, playSound, attackImpulse, attacker, attack.DamageMultiplier); if (limbHit == null) { return new AttackResult(); } Vector2 forceWorld = attack.TargetImpulseWorld + attack.TargetForceWorld; @@ -3000,7 +3113,7 @@ namespace Barotrauma return AddDamage(worldPosition, afflictions, stun, playSound, attackImpulse, out _, attacker); } - public AttackResult AddDamage(Vector2 worldPosition, IEnumerable afflictions, float stun, bool playSound, float attackImpulse, out Limb hitLimb, Character attacker = null) + public AttackResult AddDamage(Vector2 worldPosition, IEnumerable afflictions, float stun, bool playSound, float attackImpulse, out Limb hitLimb, Character attacker = null, float damageMultiplier = 1) { hitLimb = null; @@ -3022,10 +3135,26 @@ namespace Barotrauma } } - return DamageLimb(worldPosition, hitLimb, afflictions, stun, playSound, attackImpulse, attacker); + return DamageLimb(worldPosition, hitLimb, afflictions, stun, playSound, attackImpulse, attacker, damageMultiplier); } - public AttackResult DamageLimb(Vector2 worldPosition, Limb hitLimb, IEnumerable afflictions, float stun, bool playSound, float attackImpulse, Character attacker = null) + public void RecordKill(Character target) + { + if (!IsOnPlayerTeam) { return; } + if (GameMain.Config.KilledCreatures.Any(name => name.Equals(target.SpeciesName, StringComparison.OrdinalIgnoreCase))) { return; } + GameMain.Config.KilledCreatures.Add(target.SpeciesName); + AddEncounter(target); + } + + public void AddEncounter(Character other) + { + if (!IsOnPlayerTeam) { return; } + if (GameMain.Config.EncounteredCreatures.Any(name => name.Equals(other.SpeciesName, StringComparison.OrdinalIgnoreCase))) { return; } + GameMain.Config.EncounteredCreatures.Add(other.SpeciesName); + GameMain.Config.RecentlyEncounteredCreatures.Add(other.SpeciesName); + } + + public AttackResult DamageLimb(Vector2 worldPosition, Limb hitLimb, IEnumerable afflictions, float stun, bool playSound, float attackImpulse, Character attacker = null, float damageMultiplier = 1) { if (Removed) { return new AttackResult(); } @@ -3069,7 +3198,7 @@ namespace Barotrauma } bool wasDead = IsDead; Vector2 simPos = hitLimb.SimPosition + ConvertUnits.ToSimUnits(dir); - AttackResult attackResult = hitLimb.AddDamage(simPos, afflictions, playSound); + AttackResult attackResult = hitLimb.AddDamage(simPos, afflictions, playSound, damageMultiplier: damageMultiplier); CharacterHealth.ApplyDamage(hitLimb, attackResult); if (attacker != this) { @@ -3078,6 +3207,10 @@ namespace Barotrauma if (!wasDead) { TryAdjustAttackerSkill(attacker, -attackResult.Damage); + if (IsDead) + { + attacker?.RecordKill(this); + } } }; if (attackResult.Damage > 0) @@ -3086,7 +3219,9 @@ namespace Barotrauma hitLimb.ApplyStatusEffects(ActionType.OnDamaged, 1.0f); if (attacker != null) { - LastAttacker = attacker; + AddAttacker(attacker, attackResult.Damage); + AddEncounter(attacker); + attacker.AddEncounter(this); } } return attackResult; @@ -3106,7 +3241,7 @@ namespace Barotrauma float attackerSkillLevel = attacker.GetSkillLevel("weapons"); attacker.Info?.IncreaseSkillLevel("weapons", -healthChange * SkillSettings.Current.SkillIncreasePerHostileDamage / Math.Max(attackerSkillLevel, 1.0f), - attacker.WorldPosition + Vector2.UnitY * 100.0f); + attacker.Position + Vector2.UnitY * 100.0f); } } else if (healthChange > 0.0f) @@ -3114,7 +3249,7 @@ namespace Barotrauma float attackerSkillLevel = attacker.GetSkillLevel("medical"); attacker.Info?.IncreaseSkillLevel("medical", healthChange * SkillSettings.Current.SkillIncreasePerFriendlyHealed / Math.Max(attackerSkillLevel, 1.0f), - attacker.WorldPosition + Vector2.UnitY * 100.0f); + attacker.Position + Vector2.UnitY * 100.0f); } } @@ -3253,7 +3388,7 @@ namespace Barotrauma GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.Status }); } - IsDead = true; + isDead = true; ApplyStatusEffects(ActionType.OnDeath, 1.0f); @@ -3293,9 +3428,9 @@ namespace Barotrauma AnimController.movement = Vector2.Zero; AnimController.TargetMovement = Vector2.Zero; - for (int i = 0; i < selectedItems.Length; i++) + foreach (Item heldItem in HeldItems.ToList()) { - if (selectedItems[i] != null) selectedItems[i].Drop(this); + heldItem.Drop(this); } SelectedConstruction = null; @@ -3325,7 +3460,7 @@ namespace Barotrauma return; } - IsDead = false; + isDead = false; if (aiTarget != null) { @@ -3373,10 +3508,12 @@ namespace Barotrauma base.Remove(); - if (selectedItems[0] != null) { selectedItems[0].Drop(this); } - if (selectedItems[1] != null) { selectedItems[1].Drop(this); } + foreach (Item heldItem in HeldItems.ToList()) + { + heldItem.Drop(this); + } - if (info != null) { info.Remove(); } + info?.Remove(); #if CLIENT GameMain.GameSession?.CrewManager?.KillCharacter(this); @@ -3388,12 +3525,9 @@ namespace Barotrauma if (Inventory != null) { - foreach (Item item in Inventory.Items) + foreach (Item item in Inventory.AllItems) { - if (item != null) - { - Spawner?.AddToRemoveQueue(item); - } + Spawner?.AddToRemoveQueue(item); } } @@ -3421,18 +3555,13 @@ namespace Barotrauma public void SaveInventory(Inventory inventory, XElement parentElement) { - var items = Array.FindAll(inventory.Items, i => i != null).Distinct(); + var items = inventory.AllItems.Distinct(); foreach (Item item in items) { item.Submarine = inventory.Owner.Submarine; var itemElement = item.Save(parentElement); - List slotIndices = new List(); - for (int i = 0; i < inventory.Capacity; i++) - { - if (inventory.Items[i] == item) { slotIndices.Add(i); } - } - + List slotIndices = inventory.FindIndices(item); itemElement.Add(new XAttribute("i", string.Join(",", slotIndices))); foreach (ItemContainer container in item.GetComponents()) @@ -3446,10 +3575,10 @@ namespace Barotrauma public void SpawnInventoryItems(Inventory inventory, XElement itemData) { - SpawnInventoryItemsRecursive(inventory, itemData); + SpawnInventoryItemsRecursive(inventory, itemData, new List()); } - private void SpawnInventoryItemsRecursive(Inventory inventory, XElement element) + private void SpawnInventoryItemsRecursive(Inventory inventory, XElement element, List extraDuffelBags) { foreach (XElement itemElement in element.Elements()) { @@ -3475,28 +3604,91 @@ namespace Barotrauma //this should not happen normally, but can occur if the character is accidentally given new job items while also loading previous items in the campaign for (int i = 0; i < inventory.Capacity; i++) { - if (slotIndices.Contains(i) && inventory.Items[i] != null && inventory.Items[i] != newItem) + if (slotIndices.Contains(i)) { - DebugConsole.ThrowError($"Error while loading character inventory data. The slot {i} was already occupied by the item \"{inventory.Items[i].Name} ({inventory.Items[i].ID})\" when loading the item \"{newItem.Name} ({newItem.ID})\""); - inventory.Items[i].Drop(null, createNetworkEvent: false); + var existingItem = inventory.GetItemAt(i); + if (existingItem != null && existingItem != newItem && (existingItem.prefab != newItem.prefab || existingItem.Prefab.MaxStackSize == 1)) + { + DebugConsole.ThrowError($"Error while loading character inventory data. The slot {i} was already occupied by the item \"{existingItem.Name} ({existingItem.ID})\" when loading the item \"{newItem.Name} ({newItem.ID})\""); + existingItem.Drop(null, createNetworkEvent: false); + } } } - inventory.TryPutItem(newItem, slotIndices[0], false, false, null); - newItem.ParentInventory = inventory; - - //force the item to the correct slots - // e.g. putting the item in a hand slot will also put it in the first available Any-slot, - // which may not be where it actually was - for (int i = 0; i < inventory.Capacity; i++) + bool canBePutInOriginalInventory = true; + if (slotIndices[0] >= inventory.Capacity) { - if (slotIndices.Contains(i)) + canBePutInOriginalInventory = false; + //legacy support: before item stacking was implemented, revolver for example had a separate slot for each bullet + //now there's just one, try to put the extra items where they fit (= stack them) + for (int i = 0; i < inventory.Capacity; i++) { - inventory.Items[i] = newItem; + if (inventory.CanBePut(newItem, i)) + { + slotIndices[0] = i; + canBePutInOriginalInventory = true; + break; + } } - else if (inventory.Items[i] == newItem) + } + + if (canBePutInOriginalInventory) + { + inventory.TryPutItem(newItem, slotIndices[0], false, false, null); + newItem.ParentInventory = inventory; + + //force the item to the correct slots + // e.g. putting the item in a hand slot will also put it in the first available Any-slot, + // which may not be where it actually was + for (int i = 0; i < inventory.Capacity; i++) { - inventory.Items[i] = null; + if (slotIndices.Contains(i)) + { + if (!inventory.GetItemsAt(i).Contains(newItem)) { inventory.ForceToSlot(newItem, i); } + } + else if (inventory.FindIndices(newItem).Contains(i)) + { + inventory.ForceRemoveFromSlot(newItem, i); + } + } + } + else + { + // In case the inventory capacity is smaller than it was when saving: + // 1) Spawn a new duffel bag if none yet spawned or if the existing ones aren't enough + if (extraDuffelBags.None(i => i.OwnInventory.CanBePut(newItem)) && ItemPrefab.Find(null, "duffelbag") is ItemPrefab duffelBagPrefab) + { + var hull = Hull.FindHull(WorldPosition, guess: CurrentHull); + var mainSub = Submarine.MainSubs.FirstOrDefault(s => s.TeamID == TeamID); + if ((hull == null || hull.Submarine != mainSub) && mainSub != null) + { + var wp = WayPoint.GetRandom(spawnType: SpawnType.Cargo, sub: mainSub) ?? WayPoint.GetRandom(sub: mainSub); + if (wp != null) + { + hull = Hull.FindHull(wp.WorldPosition); + } + } + var newDuffelBag = new Item(duffelBagPrefab, + hull != null ? CargoManager.GetCargoPos(hull, duffelBagPrefab) : Position, + hull?.Submarine ?? Submarine); + extraDuffelBags.Add(newDuffelBag); +#if SERVER + Spawner.CreateNetworkEvent(newDuffelBag, false); +#endif + } + + // 2) Find a slot for the new item + for (int i = 0; i < extraDuffelBags.Count; i++) + { + var duffelBag = extraDuffelBags[i]; + for (int j = 0; j < duffelBag.OwnInventory.Capacity; j++) + { + if (duffelBag.OwnInventory.TryPutItem(newItem, j, false, false, null)) + { + newItem.ParentInventory = duffelBag.OwnInventory; + break; + } + } } } @@ -3506,7 +3698,7 @@ namespace Barotrauma { if (itemContainerIndex >= itemContainers.Count) break; if (!childInvElement.Name.ToString().Equals("inventory", StringComparison.OrdinalIgnoreCase)) { continue; } - SpawnInventoryItemsRecursive(itemContainers[itemContainerIndex].Inventory, childInvElement); + SpawnInventoryItemsRecursive(itemContainers[itemContainerIndex].Inventory, childInvElement, extraDuffelBags); itemContainerIndex++; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index aa4e3ce3a..38435fe0d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -171,11 +171,8 @@ namespace Barotrauma if (Character.Inventory != null) { - int cardSlotIndex = Character.Inventory.FindLimbSlot(InvSlotType.Card); - if (cardSlotIndex < 0) return disguiseName; - - var idCard = Character.Inventory.Items[cardSlotIndex]; - if (idCard == null) return disguiseName; + var idCard = Character.Inventory.GetItemInLimbSlot(InvSlotType.Card); + if (idCard == null) { return disguiseName; } //Disguise as the ID card name if it's equipped string[] readTags = idCard.Tags.Split(','); @@ -294,19 +291,15 @@ namespace Barotrauma if (Character.Inventory != null) { - int cardSlotIndex = Character.Inventory.FindLimbSlot(InvSlotType.Card); - if (cardSlotIndex >= 0) + idCard = Character.Inventory.GetItemInLimbSlot(InvSlotType.Card)?.GetComponent(); + if (idCard != null) { - idCard = Character.Inventory.Items[cardSlotIndex].GetComponent(); - - if (idCard != null) - { #if CLIENT - GetDisguisedSprites(idCard); + GetDisguisedSprites(idCard); #endif - return; - } + return; } + } } @@ -352,7 +345,7 @@ namespace Barotrauma public CauseOfDeath CauseOfDeath; - public Character.TeamType TeamID; + public CharacterTeamType TeamID; private readonly NPCPersonalityTrait personalityTrait; @@ -445,6 +438,7 @@ namespace Barotrauma { if (ragdoll == null) { + // TODO: support for variants string speciesName = SpeciesName; bool isHumanoid = CharacterConfigElement.GetAttributeBool("humanoid", speciesName.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase)); ragdoll = isHumanoid @@ -472,6 +466,7 @@ namespace Barotrauma XDocument doc = CharacterPrefab.FindBySpeciesName(_speciesName)?.XDocument; if (doc == null) { return; } CharacterConfigElement = doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root; + // TODO: support for variants head = new HeadInfo(); HasGenders = CharacterConfigElement.GetAttributeBool("genders", false); if (HasGenders) @@ -540,6 +535,7 @@ namespace Barotrauma doc = XMLExtensions.TryLoadXml(file); } if (doc == null) { return; } + // TODO: support for variants CharacterConfigElement = doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root; HasGenders = CharacterConfigElement.GetAttributeBool("genders", false); if (HasGenders && gender == Gender.None) @@ -906,7 +902,7 @@ namespace Barotrauma return (int)(salary * Job.Prefab.PriceMultiplier); } - public void IncreaseSkillLevel(string skillIdentifier, float increase, Vector2 worldPos) + public void IncreaseSkillLevel(string skillIdentifier, float increase, Vector2 pos) { if (Job == null || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) || Character == null) { return; } @@ -920,15 +916,10 @@ namespace Barotrauma float newLevel = Job.GetSkillLevel(skillIdentifier); - OnSkillChanged(skillIdentifier, prevLevel, newLevel, worldPos); - - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer && !MathUtils.NearlyEqual(newLevel, prevLevel)) - { - GameMain.NetworkMember.CreateEntityEvent(Character, new object[] { NetEntityEvent.Type.UpdateSkills }); - } + OnSkillChanged(skillIdentifier, prevLevel, newLevel, pos); } - public void SetSkillLevel(string skillIdentifier, float level, Vector2 worldPos) + public void SetSkillLevel(string skillIdentifier, float level, Vector2 pos) { if (Job == null) { return; } @@ -936,13 +927,13 @@ namespace Barotrauma if (skill == null) { Job.Skills.Add(new Skill(skillIdentifier, level)); - OnSkillChanged(skillIdentifier, 0.0f, level, worldPos); + OnSkillChanged(skillIdentifier, 0.0f, level, pos); } else { float prevLevel = skill.Level; skill.Level = level; - OnSkillChanged(skillIdentifier, prevLevel, skill.Level, worldPos); + OnSkillChanged(skillIdentifier, prevLevel, skill.Level, pos); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs index 36c9befdf..e5802eae4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; -using Barotrauma.IO; using System.Linq; -using System.Text; using System.Xml.Linq; using Microsoft.Xna.Framework; @@ -25,11 +23,12 @@ namespace Barotrauma public string Name { get; private set; } public string Identifier { get; private set; } public string FilePath { get; private set; } + public string VariantOf { get; private set; } + public ContentPackage ContentPackage { get; private set; } public XDocument XDocument { get; private set; } - public static IEnumerable ConfigFilePaths => Prefabs.Select(p => p.FilePath); public static IEnumerable ConfigFiles => Prefabs.Select(p => p.XDocument); @@ -80,22 +79,30 @@ namespace Barotrauma DebugConsole.ThrowError($"Duplicate path: {filePath}"); return false; } - XElement mainElement = doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root; - var name = mainElement.GetAttributeString("name", null); - if (name != null) + XElement mainElement = doc.Root; + if (doc.Root.IsCharacterVariant()) { - DebugConsole.NewMessage($"Error in {filePath}: 'name' is deprecated! Use 'speciesname' instead.", Color.Orange); + if (!CheckSpeciesName(mainElement, filePath, out string n)) { return false; } + string inherit = mainElement.GetAttributeString("inherit", null); + string id = n.ToLowerInvariant(); + Prefabs.Add(new CharacterPrefab + { + Name = n, + OriginalName = n, + Identifier = id, + FilePath = filePath, + ContentPackage = contentPackage, + XDocument = doc, + VariantOf = inherit + }, isOverride: false); + return true; } - else + else if (doc.Root.IsOverride()) { - name = mainElement.GetAttributeString("speciesname", string.Empty); + mainElement = doc.Root.FirstElement(); } - if (string.IsNullOrWhiteSpace(name)) - { - DebugConsole.ThrowError($"No species name defined for: {filePath}"); - return false; - } - var identifier = name.ToLowerInvariant(); + if (!CheckSpeciesName(mainElement, filePath, out string name)) { return false; } + string identifier = name.ToLowerInvariant(); Prefabs.Add(new CharacterPrefab { Name = name, @@ -109,6 +116,25 @@ namespace Barotrauma return true; } + public static bool CheckSpeciesName(XElement mainElement, string filePath, out string name) + { + name = mainElement.GetAttributeString("name", null); + if (name != null) + { + DebugConsole.NewMessage($"Error in {filePath}: 'name' is deprecated! Use 'speciesname' instead.", Color.Orange); + } + else + { + name = mainElement.GetAttributeString("speciesname", string.Empty); + } + if (string.IsNullOrWhiteSpace(name)) + { + DebugConsole.ThrowError($"No species name defined for: {filePath}"); + return false; + } + return true; + } + public static void LoadAll() { foreach (ContentFile file in ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.Character)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs index 8c87f2f5d..a6b266d9e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Xml.Linq; using System; +using Barotrauma.Extensions; namespace Barotrauma { @@ -73,7 +74,10 @@ namespace Barotrauma else if (Strength < ActiveThreshold) { DeactivateHusk(); - character.SpeechImpediment = 100; + if (Prefab is AfflictionPrefabHusk { CauseSpeechImpediment: false }) + { + character.SpeechImpediment = 100; + } State = InfectionState.Transition; } else if (Strength < Prefab.MaxStrength) @@ -118,13 +122,25 @@ namespace Barotrauma { huskAppendage = AttachHuskAppendage(character, Prefab.Identifier); } - character.NeedsAir = false; - character.SpeechImpediment = 100; + + if (Prefab is AfflictionPrefabHusk { NeedsAir: false }) + { + character.NeedsAir = false; + } + + if (Prefab is AfflictionPrefabHusk { CauseSpeechImpediment: false }) + { + character.SpeechImpediment = 100; + } } private void DeactivateHusk() { - character.NeedsAir = character.Params.MainElement.GetAttributeBool("needsair", false); + if (Prefab is AfflictionPrefabHusk { NeedsAir: false }) + { + character.NeedsAir = character.Params.MainElement.GetAttributeBool("needsair", false); + } + if (huskAppendage != null) { huskAppendage.ForEach(l => character.AnimController.RemoveLimb(l)); @@ -154,6 +170,10 @@ namespace Barotrauma } } + //character already in remove queue (being removed by something else, for example a modded affliction that uses AfflictionHusk as the base) + // -> don't spawn the AI husk + if (Entity.Spawner.IsInRemoveQueue(character)) { return; } + //create the AI husk in a coroutine to ensure that we don't modify the character list while enumerating it CoroutineManager.StartCoroutine(CreateAIHusk()); } @@ -179,7 +199,7 @@ namespace Barotrauma if (husk.Info != null) { husk.Info.Character = husk; - husk.Info.TeamID = Character.TeamType.None; + husk.Info.TeamID = CharacterTeamType.None; } foreach (Limb limb in husk.AnimController.Limbs) @@ -201,17 +221,16 @@ namespace Barotrauma if (character.Inventory != null && husk.Inventory != null) { - if (character.Inventory.Items.Length != husk.Inventory.Items.Length) + if (character.Inventory.Capacity != husk.Inventory.Capacity) { string errorMsg = "Failed to move items from the source character's inventory into a husk's inventory (inventory sizes don't match)"; DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("AfflictionHusk.CreateAIHusk:InventoryMismatch", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); yield return CoroutineStatus.Success; } - for (int i = 0; i < character.Inventory.Items.Length && i < husk.Inventory.Items.Length; i++) + for (int i = 0; i < character.Inventory.Capacity && i < husk.Inventory.Capacity; i++) { - if (character.Inventory.Items[i] == null) continue; - husk.Inventory.TryPutItem(character.Inventory.Items[i], i, true, false, null); + character.Inventory.GetItemsAt(i).ForEachMod(item => husk.Inventory.TryPutItem(item, i, true, false, null)); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index 39f9edb2b..e9367b576 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -90,6 +90,10 @@ namespace Barotrauma AttachLimbName = null; AttachLimbType = LimbType.None; } + + SendMessages = element.GetAttributeBool("sendmessages", true); + CauseSpeechImpediment = element.GetAttributeBool("causespeechimpediment", true); + NeedsAir = element.GetAttributeBool("needsair", false); } // Use any of these to define which limb the appendage is attached to. @@ -101,6 +105,10 @@ namespace Barotrauma public readonly string HuskedSpeciesName; public readonly string[] TargetSpecies; public const string Tag = "[speciesname]"; + + public readonly bool SendMessages; + public readonly bool CauseSpeechImpediment; + public readonly bool NeedsAir; } class AfflictionPrefab : IPrefab, IDisposable @@ -572,7 +580,7 @@ namespace Barotrauma IconColors = element.GetAttributeColorArray("iconcolors", null); AchievementOnRemoved = element.GetAttributeString("achievementonremoved", ""); - + foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index d2ffd65ca..b23faa20c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -214,7 +214,7 @@ namespace Barotrauma InitProjSpecific(null, character); } - public CharacterHealth(XElement element, Character character) + public CharacterHealth(XElement element, Character character, XElement limbHealthElement = null) { this.Character = character; InitIrremovableAfflictions(); @@ -224,7 +224,8 @@ namespace Barotrauma minVitality = character.IsHuman ? -100.0f : 0.0f; limbHealths.Clear(); - foreach (XElement subElement in element.Elements()) + limbHealthElement ??= element; + foreach (XElement subElement in limbHealthElement.Elements()) { if (!subElement.Name.ToString().Equals("limb", StringComparison.OrdinalIgnoreCase)) { continue; } limbHealths.Add(new LimbHealth(subElement, this)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs index bffa27eca..6e930be09 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs @@ -86,6 +86,11 @@ namespace Barotrauma private void ParseAfflictionTypes() { + if (string.IsNullOrWhiteSpace(rawAfflictionTypeString)) + { + parsedAfflictionTypes = new string[0]; + return; + } string[] splitValue = rawAfflictionTypeString.Split(',', ','); for (int i = 0; i < splitValue.Length; i++) { @@ -96,6 +101,11 @@ namespace Barotrauma private void ParseAfflictionIdentifiers() { + if (string.IsNullOrWhiteSpace(rawAfflictionIdentifierString)) + { + parsedAfflictionIdentifiers = new string[0]; + return; + } string[] splitValue = rawAfflictionIdentifierString.Split(',', ','); for (int i = 0; i < splitValue.Length; i++) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index 65874e3d5..1f2757e3a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -330,7 +330,7 @@ namespace Barotrauma } } - public Submarine Submarine => character.Submarine; + public Submarine Submarine => character?.Submarine; public bool Hidden { @@ -340,7 +340,7 @@ namespace Barotrauma public Vector2 WorldPosition { - get { return character.Submarine == null ? Position : Position + character.Submarine.Position; } + get { return character?.Submarine == null ? Position : Position + character.Submarine.Position; } } public Vector2 Position @@ -622,6 +622,14 @@ namespace Barotrauma } attack.DamageRange = ConvertUnits.ToDisplayUnits(attack.DamageRange); } + if (character.VariantOf != null && character.Params.VariantFile != null) + { + var attackElement = character.Params.VariantFile.Root.GetChildElement("attack"); + if (attackElement != null) + { + attack.DamageMultiplier = attackElement.GetAttributeFloat("damagemultiplier", 1f); + } + } break; case "damagemodifier": DamageModifiers.Add(new DamageModifier(subElement, character.Name)); @@ -669,7 +677,7 @@ namespace Barotrauma private readonly List appliedDamageModifiers = new List(); private readonly List tempModifiers = new List(); private readonly List afflictionsCopy = new List(); - public AttackResult AddDamage(Vector2 simPosition, IEnumerable afflictions, bool playSound) + public AttackResult AddDamage(Vector2 simPosition, IEnumerable afflictions, bool playSound, float damageMultiplier = 1) { appliedDamageModifiers.Clear(); afflictionsCopy.Clear(); @@ -709,7 +717,7 @@ namespace Barotrauma } } } - float finalDamageModifier = 1.0f; + float finalDamageModifier = damageMultiplier; foreach (DamageModifier damageModifier in tempModifiers) { finalDamageModifier *= damageModifier.DamageMultiplier; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs index 1d18cf6d4..10f6c0215 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs @@ -66,8 +66,13 @@ namespace Barotrauma protected static Dictionary> allAnimations = new Dictionary>(); + private float _movementSpeed; [Serialize(1.0f, true), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = Ragdoll.MAX_SPEED, ValueStep = 0.1f)] - public float MovementSpeed { get; set; } + public float MovementSpeed + { + get => _movementSpeed; + set => _movementSpeed = value; + } [Serialize(1.0f, true, description: "The speed of the \"animation cycle\", i.e. how fast the character takes steps or moves the tail/legs/arms (the outcome depends what the clip is about)"), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2, ValueStep = 0.01f)] @@ -110,11 +115,10 @@ namespace Barotrauma [Serialize(AnimationType.NotDefined, true), Editable] public virtual AnimationType AnimationType { get; protected set; } - public static string GetDefaultFileName(string speciesName, AnimationType animType) => $"{speciesName.CapitaliseFirstInvariant()}{animType.ToString()}"; - public static string GetDefaultFile(string speciesName, AnimationType animType, ContentPackage contentPackage = null) - => Path.Combine(GetFolder(speciesName, contentPackage), $"{GetDefaultFileName(speciesName, animType)}.xml"); + public static string GetDefaultFileName(string speciesName, AnimationType animType) => $"{speciesName.CapitaliseFirstInvariant()}{animType}"; + public static string GetDefaultFile(string speciesName, AnimationType animType) => Path.Combine(GetFolder(speciesName), $"{GetDefaultFileName(speciesName, animType)}.xml"); - public static string GetFolder(string speciesName, ContentPackage contentPackage = null) + public static string GetFolder(string speciesName) { CharacterPrefab prefab = CharacterPrefab.FindBySpeciesName(speciesName); if (prefab?.XDocument == null) @@ -132,7 +136,7 @@ namespace Barotrauma { folder = Path.Combine(Path.GetDirectoryName(filePath), "Animations"); } - return folder; + return folder.CleanUpPathCrossPlatform(true); } /// @@ -163,7 +167,16 @@ namespace Barotrauma return Enum.TryParse(typeString, out AnimationType fileType) && fileType == type; } - public static T GetDefaultAnimParams(string speciesName, AnimationType animType) where T : AnimationParams, new() => GetAnimParams(speciesName, animType, GetDefaultFileName(speciesName, animType)); + public static T GetDefaultAnimParams(Character character, AnimationType animType) where T : AnimationParams, new() + { + string speciesName = character.VariantOf ?? character.SpeciesName; + if (character.VariantOf != null && character.Params.VariantFile?.Root?.GetChildElement("animations")?.GetAttributeString("folder", null) != null) + { + // Use the overridden animations defined in the variant definition file. + speciesName = character.SpeciesName; + } + return GetAnimParams(speciesName, animType, GetDefaultFileName(speciesName, animType)); + } /// /// If the file name is left null, default file is selected. If fails, will select the default file. Note: Use the filename without the extensions, don't use the full path! diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs index 981e704fb..fe8a2da9b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs @@ -7,11 +7,11 @@ namespace Barotrauma { public static FishWalkParams GetDefaultAnimParams(Character character) { - return Check(character) ? GetDefaultAnimParams(character.SpeciesName, AnimationType.Walk) : Empty; + return Check(character) ? GetDefaultAnimParams(character, AnimationType.Walk) : Empty; } public static FishWalkParams GetAnimParams(Character character, string fileName = null) { - return Check(character) ? GetAnimParams(character.SpeciesName, AnimationType.Walk, fileName) : Empty; + return Check(character) ? GetAnimParams(character.VariantOf ?? character.SpeciesName, AnimationType.Walk, fileName) : Empty; } protected static FishWalkParams Empty = new FishWalkParams(); @@ -23,11 +23,11 @@ namespace Barotrauma { public static FishRunParams GetDefaultAnimParams(Character character) { - return Check(character) ? GetDefaultAnimParams(character.SpeciesName, AnimationType.Run) : Empty; + return Check(character) ? GetDefaultAnimParams(character, AnimationType.Run) : Empty; } public static FishRunParams GetAnimParams(Character character, string fileName = null) { - return Check(character) ? GetAnimParams(character.SpeciesName, AnimationType.Run, fileName) : Empty; + return Check(character) ? GetAnimParams(character.VariantOf ?? character.SpeciesName, AnimationType.Run, fileName) : Empty; } protected static FishRunParams Empty = new FishRunParams(); @@ -37,10 +37,10 @@ namespace Barotrauma class FishSwimFastParams : FishSwimParams { - public static FishSwimFastParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams(character.SpeciesName, AnimationType.SwimFast); + public static FishSwimFastParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams(character, AnimationType.SwimFast); public static FishSwimFastParams GetAnimParams(Character character, string fileName = null) { - return GetAnimParams(character.SpeciesName, AnimationType.SwimFast, fileName); + return GetAnimParams(character.VariantOf ?? character.SpeciesName, AnimationType.SwimFast, fileName); } public override void StoreSnapshot() => StoreSnapshot(); @@ -48,10 +48,10 @@ namespace Barotrauma class FishSwimSlowParams : FishSwimParams { - public static FishSwimSlowParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams(character.SpeciesName, AnimationType.SwimSlow); + public static FishSwimSlowParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams(character, AnimationType.SwimSlow); public static FishSwimSlowParams GetAnimParams(Character character, string fileName = null) { - return GetAnimParams(character.SpeciesName, AnimationType.SwimSlow, fileName); + return GetAnimParams(character.VariantOf ?? character.SpeciesName, AnimationType.SwimSlow, fileName); } public override void StoreSnapshot() => StoreSnapshot(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs index 58d4dcded..639b7f46e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs @@ -4,7 +4,7 @@ namespace Barotrauma { class HumanWalkParams : HumanGroundedParams { - public static HumanWalkParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams(character.SpeciesName, AnimationType.Walk); + public static HumanWalkParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams(character, AnimationType.Walk); public static HumanWalkParams GetAnimParams(Character character, string fileName = null) { return GetAnimParams(character.SpeciesName, AnimationType.Walk, fileName); @@ -15,7 +15,7 @@ namespace Barotrauma class HumanRunParams : HumanGroundedParams { - public static HumanRunParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams(character.SpeciesName, AnimationType.Run); + public static HumanRunParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams(character, AnimationType.Run); public static HumanRunParams GetAnimParams(Character character, string fileName = null) { return GetAnimParams(character.SpeciesName, AnimationType.Run, fileName); @@ -26,7 +26,7 @@ namespace Barotrauma class HumanSwimFastParams: HumanSwimParams { - public static HumanSwimFastParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams(character.SpeciesName, AnimationType.SwimFast); + public static HumanSwimFastParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams(character, AnimationType.SwimFast); public static HumanSwimFastParams GetAnimParams(Character character, string fileName = null) { return GetAnimParams(character.SpeciesName, AnimationType.SwimFast, fileName); @@ -38,7 +38,7 @@ namespace Barotrauma class HumanSwimSlowParams : HumanSwimParams { - public static HumanSwimSlowParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams(character.SpeciesName, AnimationType.SwimSlow); + public static HumanSwimSlowParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams(character, AnimationType.SwimSlow); public static HumanSwimSlowParams GetAnimParams(Character character, string fileName = null) { return GetAnimParams(character.SpeciesName, AnimationType.SwimSlow, fileName); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index a76ecdf96..f2f59d231 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -78,6 +78,8 @@ namespace Barotrauma public readonly string File; + public XDocument VariantFile { get; private set; } + public readonly List SubParams = new List(); public readonly List Sounds = new List(); public readonly List BloodEmitters = new List(); @@ -100,6 +102,32 @@ namespace Barotrauma public bool Load() { bool success = base.Load(File); + if (doc.Root.IsCharacterVariant()) + { + VariantFile = doc; + var original = CharacterPrefab.FindBySpeciesName(doc.Root.GetAttributeString("inherit", string.Empty)); + success = Load(original.FilePath); + CreateSubParams(); + TryLoadOverride(this, VariantFile.Root, SerializableProperties); + foreach (XElement subElement in VariantFile.Root.Elements()) + { + var matchingParams = SubParams.FirstOrDefault(p => p.Name.Equals(subElement.Name.ToString(), StringComparison.OrdinalIgnoreCase)); + if (matchingParams != null) + { + TryLoadOverride(matchingParams, subElement, matchingParams.SerializableProperties); + // TODO: Make recursive? In practice we don't have to go deeper than this, but the implementation would be a lot cleaner with recursion. + foreach (XElement subSubElement in subElement.Elements()) + { + matchingParams = matchingParams.SubParams.FirstOrDefault(p => p.Name.Equals(subSubElement.Name.ToString(), StringComparison.OrdinalIgnoreCase)); + if (matchingParams != null) + { + TryLoadOverride(matchingParams, subSubElement, matchingParams.SerializableProperties); + } + } + } + } + return success; + } if (string.IsNullOrEmpty(SpeciesName) && MainElement != null) { //backwards compatibility @@ -111,6 +139,8 @@ namespace Barotrauma public bool Save(string fileNameWithoutExtension = null) { + // Disable saving variants for now. Making it work probably requires more work. + if (VariantFile != null) { return false; } Serialize(); return base.Save(fileNameWithoutExtension, new XmlWriterSettings { @@ -181,7 +211,19 @@ namespace Barotrauma } } - public bool Deserialize(XElement element = null, bool alsoChildren = true, bool recursive = true) + private void TryLoadOverride(object parentObject, XElement element, Dictionary properties) + { + foreach (var property in properties) + { + var matchingAttribute = element.GetAttribute(property.Key); + if (matchingAttribute != null) + { + property.Value.TrySetValue(parentObject, matchingAttribute.Value); + } + } + } + + public bool Deserialize(XElement element = null, bool alsoChildren = true, bool recursive = true, bool loadDefaultValues = true) { if (base.Deserialize(element)) { @@ -486,17 +528,21 @@ namespace Barotrauma [Serialize(true, true, description: "Enforce aggressive behavior if the creature is spawned as a target of a monster mission."), Editable()] public bool EnforceAggressiveBehaviorForMissions { get; private set; } - [Serialize(true, true, description: "Should the character target or ignore walls when it's outside the submarine. Doesn't have any effect if no target priority for walls is defined."), Editable()] + [Serialize(true, true, description: "Should the character target or ignore walls when it's outside the submarine."), Editable()] public bool TargetOuterWalls { get; private set; } [Serialize(false, true, description: "If enabled, the character chooses randomly from the available attacks. The priority is used as a weight for weighted random."), Editable()] public bool RandomAttack { get; private set; } + [Serialize(false, true, description:"Can the character open doors and hatches without a proper id card? Only applies on humanoids.")] + public bool Infiltrate { get; private set; } + public IEnumerable Targets => targets; protected readonly List targets = new List(); public AIParams(XElement element, CharacterParams character) : base(element, character) { + if (element == null) { return; } element.GetChildElements("target").ForEach(t => TryAddTarget(t, out _)); element.GetChildElements("targetpriority").ForEach(t => TryAddTarget(t, out _)); } @@ -588,10 +634,13 @@ namespace Barotrauma public bool IgnoreContained { get; set; } [Serialize(false, true, description: "Should the target be ignored while the creature is inside. Doesn't matter where the target is."), Editable] - public bool IgnoreWhileInside { get; set; } + public bool IgnoreInside { get; set; } [Serialize(false, true, description: "Should the target be ignored while the creature is outside. Doesn't matter where the target is."), Editable] - public bool IgnoreWhileOutside { get; set; } + public bool IgnoreOutside { get; set; } + + [Serialize(false, true)] + public bool IgnoreIncapacitated { get; set; } [Serialize(0f, true, description: "Use to define a distance at which the creature starts the sweeping movement."), Editable(MinValueFloat = 0, MaxValueFloat = 10000, ValueStep = 1, DecimalCount = 0)] public float SweepDistance { get; private set; } @@ -602,6 +651,9 @@ namespace Barotrauma [Serialize(1f, true, description: "How quickly the sweep direction changes. Uses the sine wave pattern."), Editable(MinValueFloat = 0, MaxValueFloat = 10, ValueStep = 0.1f, DecimalCount = 2)] public float SweepSpeed { get; private set; } + [Serialize(0f, true, description: "How much damage the protected target should take from an attacker before the creature starts defending it.")] + public float Threshold { get; private set; } + public TargetParams(XElement element, CharacterParams character) : base(element, character) { } public TargetParams(string tag, AIState state, float priority, CharacterParams character) : base(CreateNewElement(tag, state, priority), character) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/EditableParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/EditableParams.cs index 1a7fa97f7..77fff0fd0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/EditableParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/EditableParams.cs @@ -44,14 +44,14 @@ namespace Barotrauma protected virtual bool Deserialize(XElement element = null) { - element = element ?? MainElement; + element ??= MainElement; SerializableProperties = SerializableProperty.DeserializeProperties(this, element); return SerializableProperties != null; } protected virtual bool Serialize(XElement element = null) { - element = element ?? MainElement; + element ??= MainElement; if (element == null) { DebugConsole.ThrowError("[EditableParams] The XML element is null!"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs index fc0d4fe22..67880787e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs @@ -110,7 +110,7 @@ namespace Barotrauma { folder = Path.Combine(Path.GetDirectoryName(filePath), "Ragdolls") + Path.DirectorySeparatorChar; } - return folder; + return folder.CleanUpPathCrossPlatform(correctFilenameCase: true); } public static T GetDefaultRagdollParams(string speciesName) where T : RagdollParams, new() => GetRagdollParams(speciesName, GetDefaultFileName(speciesName)); @@ -136,7 +136,7 @@ namespace Barotrauma string folder = GetFolder(speciesName); if (Directory.Exists(folder)) { - var files = Directory.GetFiles(folder); + List files = Directory.GetFiles(folder).ToList(); if (files.None()) { DebugConsole.ThrowError($"[RagdollParams] Could not find any ragdoll files from the folder: {folder}. Using the default ragdoll."); @@ -364,6 +364,21 @@ namespace Barotrauma } } #endif + + private bool variantScaleApplied; + public void ApplyVariantScale(XDocument variantFile) + { + if (variantScaleApplied) { return; } + if (variantFile == null) { return; } + var scaleMultiplier = variantFile.Root.GetChildElement("ragdoll")?.GetAttributeFloat("scalemultiplier", 1f); + if (scaleMultiplier.HasValue) + { + JointScale *= scaleMultiplier.Value; + LimbScale *= scaleMultiplier.Value; + } + variantScaleApplied = true; + } + #endregion #region Memento diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs index d8c8483b2..715d7bb40 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs @@ -57,13 +57,13 @@ namespace Barotrauma { public static string Folder = "Data/ContentPackages/"; - private static List regularPackages = new List(); + private static readonly List regularPackages = new List(); public static IReadOnlyList RegularPackages { get { return regularPackages; } } - private static List corePackages = new List(); + private static readonly List corePackages = new List(); public static IReadOnlyList CorePackages { get { return corePackages; } @@ -105,7 +105,7 @@ namespace Barotrauma }; //at least one file of each these types is required in core content packages - private static HashSet corePackageRequiredFiles = new HashSet + private static readonly HashSet corePackageRequiredFiles = new HashSet { ContentType.Jobs, ContentType.Item, @@ -141,8 +141,8 @@ namespace Barotrauma } public static bool IngameModSwap = false; - - public string Name { get; set; } + + public string Name { get; set; } = string.Empty; public string Path { @@ -208,9 +208,9 @@ namespace Barotrauma } - private List files; - private List filesToAdd; - private List filesToRemove; + private readonly List files; + private readonly List filesToAdd; + private readonly List filesToRemove; public IReadOnlyList Files @@ -238,6 +238,12 @@ namespace Barotrauma get { return Files.Any(f => MultiplayerIncompatibleContent.Contains(f.Type)); } } + public bool IsCorrupt + { + get; + private set; + } + private ContentPackage() { files = new List(); @@ -256,7 +262,8 @@ namespace Barotrauma if (doc?.Root == null) { - DebugConsole.ThrowError("Couldn't load content package \"" + filePath + "\"!"); + DebugConsole.ThrowError("Couldn't load content package \"" + filePath + "\"!"); + IsCorrupt = true; return; } @@ -621,14 +628,12 @@ namespace Barotrauma { case ContentType.Character: XDocument doc = XMLExtensions.TryLoadXml(file.Path); - var rootElement = doc.Root; - var element = rootElement.IsOverride() ? rootElement.FirstElement() : rootElement; - var ragdollFolder = RagdollParams.GetFolder(doc, file.Path).CleanUpPathCrossPlatform(true); + var ragdollFolder = RagdollParams.GetFolder(doc, file.Path); if (Directory.Exists(ragdollFolder)) { Directory.GetFiles(ragdollFolder, "*.xml").ForEach(f => filePaths.Add(f)); } - var animationFolder = AnimationParams.GetFolder(doc, file.Path).CleanUpPathCrossPlatform(true); + var animationFolder = AnimationParams.GetFolder(doc, file.Path); if (Directory.Exists(animationFolder)) { Directory.GetFiles(animationFolder, "*.xml").ForEach(f => filePaths.Add(f)); @@ -764,7 +769,8 @@ namespace Barotrauma foreach (string filePath in files) { - AddPackage(new ContentPackage(filePath)); + var newPackage = new ContentPackage(filePath); + if (!newPackage.IsCorrupt) { AddPackage(newPackage); } } IEnumerable modDirectories = Directory.GetDirectories("Mods"); @@ -780,21 +786,25 @@ namespace Barotrauma } else if (File.Exists(modFilePath)) { - AddPackage(new ContentPackage(modFilePath)); + var newPackage = new ContentPackage(modFilePath); + if (!newPackage.IsCorrupt) + { + AddPackage(newPackage); + } } } SortContentPackages(p => prevRegularPackages.IndexOf(p.Name.ToLowerInvariant())); GameMain.Config?.SortContentPackages(); } - public static void SortContentPackages(Func order, bool refreshAll = false) + public static void SortContentPackages(Func order, bool refreshAll = false, GameSettings config = null) { var ordered = regularPackages .OrderBy(p => order(p)) .ThenBy(p => regularPackages.IndexOf(p)) .ToList(); regularPackages.Clear(); regularPackages.AddRange(ordered); - GameMain.Config?.SortContentPackages(refreshAll); + (config ?? GameMain.Config)?.SortContentPackages(refreshAll); } public void Delete() diff --git a/Barotrauma/BarotraumaShared/SharedSource/CoroutineManager.cs b/Barotrauma/BarotraumaShared/SharedSource/CoroutineManager.cs index 4091faa8c..f149339d6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/CoroutineManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/CoroutineManager.cs @@ -106,24 +106,7 @@ namespace Barotrauma { lock (Coroutines) { - Coroutines.ForEach(c => - { - if (c.Name == name) - { - c.AbortRequested = true; - if (c.Thread != null) - { - bool joined = false; - while (!joined) - { -#if CLIENT - CrossThread.ProcessTasks(); -#endif - joined = c.Thread.Join(TimeSpan.FromMilliseconds(500)); - } - } - } - }); + HandleCoroutineStopping(c => c.Name == name); Coroutines.RemoveAll(c => c.Name == name); } } @@ -132,10 +115,33 @@ namespace Barotrauma { lock (Coroutines) { + HandleCoroutineStopping(c => c == handle); Coroutines.RemoveAll(c => c == handle); } } + private static void HandleCoroutineStopping(Func filter) + { + foreach (CoroutineHandle coroutine in Coroutines) + { + if (filter(coroutine)) + { + coroutine.AbortRequested = true; + if (coroutine.Thread != null) + { + bool joined = false; + while (!joined) + { +#if CLIENT + CrossThread.ProcessTasks(); +#endif + joined = coroutine.Thread.Join(TimeSpan.FromMilliseconds(500)); + } + } + } + } + } + public static void ExecuteCoroutineThread(CoroutineHandle handle) { try diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index a0a73da59..307362d14 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -833,6 +833,8 @@ namespace Barotrauma else { NewMessage("Level seed: " + Level.Loaded.Seed); + NewMessage("Level size: " + Level.Loaded.Size.X+"x"+ Level.Loaded.Size.Y); + NewMessage("Minimum main path width: " + (Level.Loaded.LevelData?.MinMainPathWidth?.ToString() ?? "unknown")); } },null)); @@ -1298,6 +1300,7 @@ namespace Barotrauma { if (item.CurrentHull != null && item.HasTag("ballast") && item.GetComponent() is { } pump) { + if (item.CurrentHull.BallastFlora != null) { continue; } pumps.Add(pump); } } @@ -1312,8 +1315,8 @@ namespace Barotrauma } Pump random = pumps.GetRandom(); - random.InfectBallast(prefab.Identifier); - NewMessage($"Infected {random.Name} with {prefab.Identifier}.", Color.Green); + random.InfectBallast(prefab.Identifier, allowMultiplePerShip: true); + NewMessage($"Infected {random.Name} with {prefab.Identifier} in {random.Item.CurrentHull.DisplayName}.", Color.Green); return; } @@ -1459,6 +1462,28 @@ namespace Barotrauma NewMessage("Set packet duplication to " + (int)(duplicates * 100) + "%.", Color.White); })); +#if DEBUG + commands.Add(new Command("storeinfo", "", (string[] args) => + { + if (GameMain.GameSession?.Map?.CurrentLocation is Location location) + { + + var msg = "--- Location: " + location.Name + " ---"; + msg += "\nBalance: " + location.StoreCurrentBalance; + msg += "\nPrice modifier: " + location.StorePriceModifier + "%"; + msg += "\nDaily specials:"; + location.DailySpecials.ForEach(i => msg += "\n - " + i.Name); + msg += "\nRequested goods:"; + location.RequestedGoods.ForEach(i => msg += "\n - " + i.Name); + NewMessage(msg); + } + else + { + NewMessage("No current location set, can't show store info."); + } + })); +#endif + //"dummy commands" that only exist so that the server can give clients permissions to use them //TODO: alphabetical order? commands.Add(new Command("control", "control [character name]: Start controlling the specified character (client-only).", null, () => @@ -1469,6 +1494,7 @@ namespace Barotrauma commands.Add(new Command("lighting|lights", "Toggle lighting on/off (client-only).", null, isCheat: true)); commands.Add(new Command("ambientlight", "ambientlight [color]: Change the color of the ambient light in the level.", null, isCheat: true)); commands.Add(new Command("debugdraw", "Toggle the debug drawing mode on/off (client-only).", null, isCheat: true)); + commands.Add(new Command("togglevoicechatfilters", "Toggle the radio/muffle filters in the voice chat (client-only).", null, isCheat: false)); commands.Add(new Command("togglehud|hud", "Toggle the character HUD (inventories, icons, buttons, etc) on/off (client-only).", null)); commands.Add(new Command("toggleupperhud", "Toggle the upper part of the ingame HUD (chatbox, crewmanager) on/off (client-only).", null)); commands.Add(new Command("toggleitemhighlights", "Toggle the item highlight effect on/off (client-only).", null)); @@ -1766,7 +1792,7 @@ namespace Barotrauma if (GameMain.GameSession != null) { //TODO: a way to select which team to spawn to? - spawnedCharacter.TeamID = Character.Controlled != null ? Character.Controlled.TeamID : Character.TeamType.Team1; + spawnedCharacter.TeamID = Character.Controlled != null ? Character.Controlled.TeamID : CharacterTeamType.Team1; #if CLIENT GameMain.GameSession.CrewManager.AddCharacter(spawnedCharacter); #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs index 6d0d61c77..b01df332d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs @@ -59,7 +59,7 @@ namespace Barotrauma (Rand.Value(Rand.RandSync.Server) < 0.5f) ? Level.PositionType.MainPath | Level.PositionType.SidePath : Level.PositionType.Cave | Level.PositionType.Ruin, - 500.0f, 10000.0f, 30.0f); + 500.0f, 10000.0f, 30.0f, SpawnPosFilter); spawnPending = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs index d6bd7d329..be1857c50 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs @@ -1,4 +1,5 @@ using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; namespace Barotrauma @@ -11,6 +12,8 @@ namespace Barotrauma public EventPrefab Prefab => prefab; + public Func SpawnPosFilter; + public bool IsFinished { get { return isFinished; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckAfflictionAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckAfflictionAction.cs new file mode 100644 index 000000000..658c70d5c --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckAfflictionAction.cs @@ -0,0 +1,59 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + internal class CheckAfflictionAction : BinaryOptionAction + { + [Serialize("", true)] + public string Identifier { get; set; } = ""; + + [Serialize("", true)] + public string TargetTag { get; set; } = ""; + + [Serialize(LimbType.None, true, "Only check afflictions on the specified limb type")] + public LimbType TargetLimb { get; set; } + + [Serialize(true, true, "When set to false when TargetLimb is not specified prevent checking limb-specific afflictions")] + public bool AllowLimbAfflictions { get; set; } + + public CheckAfflictionAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + + protected override bool? DetermineSuccess() + { + if (string.IsNullOrWhiteSpace(Identifier) || string.IsNullOrWhiteSpace(TargetTag)) { return false; } + List targets = ParentEvent.GetTargets(TargetTag).OfType().ToList(); + + if (!(targets.FirstOrDefault() is { } target)) { return false; } + + if (TargetLimb == LimbType.None) + { + Affliction? affliction = target.CharacterHealth?.GetAffliction(Identifier, AllowLimbAfflictions); + return affliction != null; + } + + if (target.CharacterHealth == null) { return false; } + + IEnumerable afflictions = target.CharacterHealth.GetAllAfflictions().Where(affliction => + { + LimbType? limbType = target.CharacterHealth.GetAfflictionLimb(affliction)?.type; + if (limbType == null) { return false; } + + return limbType == TargetLimb || true; + }); + + return afflictions.Any(a => a.Identifier.Equals(Identifier, StringComparison.OrdinalIgnoreCase)); + } + + public override string ToDebugString() + { + return $"{ToolBox.GetDebugSymbol(HasBeenDetermined())} {nameof(CheckAfflictionAction)} -> (TargetTag: {TargetTag.ColorizeObject()}, " + + $"AfflictionIdentifier: {Identifier.ColorizeObject()}, " + + $"TargetLimb: {TargetLimb.ColorizeObject()}, " + + $"Succeeded: {succeeded.ColorizeObject()})"; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs index a16349654..1b8fd0967 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/CheckDataAction.cs @@ -1,4 +1,5 @@ #nullable enable +using System; using System.Xml.Linq; namespace Barotrauma @@ -11,6 +12,12 @@ namespace Barotrauma [Serialize("", true)] public string Condition { get; set; } = null!; + [Serialize(false, true, "Forces the comparison to use string instead of attempting to parse it as a boolean or a float first")] + public bool ForceString { get; set; } + + [Serialize(false, true, "Performs the comparison against a metadata by identifier instead of a constant value")] + public bool CheckAgainstMetadata { get; set; } + protected object? value2; protected object? value1; @@ -41,13 +48,52 @@ namespace Barotrauma Operator = PropertyConditional.GetOperatorType(op); if (Operator == PropertyConditional.OperatorType.None) { return false; } - bool? tryBoolean = TryBoolean(campaignMode, value); - if (tryBoolean != null) { return tryBoolean; } + if (CheckAgainstMetadata) + { + object? metadata1 = campaignMode.CampaignMetadata.GetValue(Identifier); + object? metadata2 = campaignMode.CampaignMetadata.GetValue(value); - bool? tryFloat = TryFloat(campaignMode, value); - if (tryFloat != null) { return tryFloat; } + if (metadata1 == null || metadata2 == null) + { + return Operator switch + { + PropertyConditional.OperatorType.Equals => metadata1 == metadata2, + PropertyConditional.OperatorType.NotEquals => metadata1 != metadata2, + _ => false + }; + } + + if (!ForceString) + { + switch (metadata1) + { + case bool bool1 when metadata2 is bool bool2: + return CompareBool(bool1, bool2) ?? false; + case float float1 when metadata2 is float float2: + return CompareFloat(float1, float2) ?? false; + } + } + + if (metadata1 is string string1 && metadata2 is string string2) + { + return CompareString(string1, string2) ?? false; + } + + return false; + } + + if (!ForceString) + { + bool? tryBoolean = TryBoolean(campaignMode, value); + if (tryBoolean != null) { return tryBoolean; } + + bool? tryFloat = TryFloat(campaignMode, value); + if (tryFloat != null) { return tryFloat; } + } + + bool? tryString = TryString(campaignMode, value); + if (tryString != null) { return tryString; } - DebugConsole.ThrowError($"{value2} ({Condition}) did not match a boolean or a float."); return false; } @@ -55,53 +101,85 @@ namespace Barotrauma { if (bool.TryParse(value, out bool b)) { - bool target = GetBool(campaignMode); - value1 = target; - value2 = b; - switch (Operator) - { - case PropertyConditional.OperatorType.Equals: - return target == b; - case PropertyConditional.OperatorType.NotEquals: - return target != b; - default: - DebugConsole.Log($"Only \"Equals\" and \"Not equals\" operators are allowed for a boolean (was {Operator} for {value})."); - return false; - } + return CompareBool(GetBool(campaignMode), b); } DebugConsole.Log($"{value} != bool"); return null; } + private bool? CompareBool(bool val1, bool val2) + { + value1 = val1; + value2 = val2; + switch (Operator) + { + case PropertyConditional.OperatorType.Equals: + return val1 == val2; + case PropertyConditional.OperatorType.NotEquals: + return val1 != val2; + default: + DebugConsole.Log($"Only \"Equals\" and \"Not equals\" operators are allowed for a boolean (was {Operator} for {val2})."); + return false; + } + } + private bool? TryFloat(CampaignMode campaignMode, string value) { if (float.TryParse(value, out float f)) { - float target = GetFloat(campaignMode); - value1 = target; - value2 = f; - switch (Operator) - { - case PropertyConditional.OperatorType.Equals: - return MathUtils.NearlyEqual(target, f); - case PropertyConditional.OperatorType.GreaterThan: - return target > f; - case PropertyConditional.OperatorType.GreaterThanEquals: - return target >= f; - case PropertyConditional.OperatorType.LessThan: - return target < f; - case PropertyConditional.OperatorType.LessThanEquals: - return target <= f; - case PropertyConditional.OperatorType.NotEquals: - return !MathUtils.NearlyEqual(target, f); - } + return CompareFloat(GetFloat(campaignMode), f); } DebugConsole.Log($"{value} != float"); return null; } - + + private bool? CompareFloat(float val1, float val2) + { + value1 = val1; + value2 = val2; + switch (Operator) + { + case PropertyConditional.OperatorType.Equals: + return MathUtils.NearlyEqual(val1, val2); + case PropertyConditional.OperatorType.GreaterThan: + return val1 > val2; + case PropertyConditional.OperatorType.GreaterThanEquals: + return val1 >= val2; + case PropertyConditional.OperatorType.LessThan: + return val1 < val2; + case PropertyConditional.OperatorType.LessThanEquals: + return val1 <= val2; + case PropertyConditional.OperatorType.NotEquals: + return !MathUtils.NearlyEqual(val1, val2); + } + + return null; + } + + private bool? TryString(CampaignMode campaignMode, string value) + { + return CompareString(GetString(campaignMode), value); + } + + private bool? CompareString(string val1, string val2) + { + value1 = val1; + value2 = val2; + bool equals = string.Equals(val1, val2, StringComparison.OrdinalIgnoreCase); + switch (Operator) + { + case PropertyConditional.OperatorType.Equals: + return equals; + case PropertyConditional.OperatorType.NotEquals: + return !equals; + default: + DebugConsole.Log($"Only \"Equals\" and \"Not equals\" operators are allowed for a string (was {Operator} for {val2})."); + return null; + } + } + protected virtual bool GetBool(CampaignMode campaignMode) { return campaignMode.CampaignMetadata.GetBoolean(Identifier); @@ -112,6 +190,11 @@ namespace Barotrauma return campaignMode.CampaignMetadata.GetFloat(Identifier); } + private string GetString(CampaignMode campaignMode) + { + return campaignMode.CampaignMetadata.GetString(Identifier); + } + public override string ToDebugString() { string condition = "?"; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ClearTagAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ClearTagAction.cs new file mode 100644 index 000000000..567fdee02 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ClearTagAction.cs @@ -0,0 +1,38 @@ +using System.Xml.Linq; +using NLog.Targets; + +namespace Barotrauma +{ + class ClearTagAction : EventAction + { + [Serialize("", true)] + public string Tag { get; set; } + + private bool isFinished; + + public ClearTagAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + + public override bool IsFinished(ref string goToLabel) => isFinished; + + public override void Reset() + { + isFinished = false; + } + + public override void Update(float deltaTime) + { + if (isFinished) { return; } + + if (!string.IsNullOrWhiteSpace(Tag) && ParentEvent.Targets.ContainsKey(Tag)) + { + ParentEvent.Targets.Remove(Tag); + } + isFinished = true; + } + + public override string ToDebugString() + { + return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(ClearTagAction)} -> (Tag: {Tag.ColorizeObject()})"; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs index 02ea64468..1b77d9040 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/ConversationAction.cs @@ -1,3 +1,4 @@ +using Barotrauma.Extensions; using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; @@ -40,9 +41,15 @@ namespace Barotrauma [Serialize(true, true)] public bool WaitForInteraction { get; set; } + [Serialize("", true, "Tag to assign to whoever invokes the conversation")] + public string InvokerTag { get; set; } + [Serialize(false, true)] public bool FadeToBlack { get; set; } + [Serialize(true, true, "Should the event end if the conversations is interrupted (e.g. if the speaker dies or falls unconscious mid-conversation). Defaults to true.")] + public bool EndEventIfInterrupted { get; set; } + [Serialize("", true)] public string EventSprite { get; set; } @@ -104,19 +111,26 @@ namespace Barotrauma { #if CLIENT dialogBox?.Close(); + GUIMessageBox.MessageBoxes.ForEachMod(mb => + { + if (mb.UserData as string == "ConversationAction") + { + (mb as GUIMessageBox)?.Close(); + } + }); #else foreach (Client c in GameMain.Server.ConnectedClients) { if (c.InGame && c.Character != null) { ServerWrite(speaker, c); } } -# endif +#endif ResetSpeaker(); dialogOpened = false; } if (Interrupted == null) { - goTo = "_end"; + if (EndEventIfInterrupted) { goTo = "_end"; } return true; } else @@ -171,7 +185,7 @@ namespace Barotrauma GameMain.NetworkMember.CreateEntityEvent(speaker, new object[] { NetEntityEvent.Type.AssignCampaignInteraction }); #endif var humanAI = speaker.AIController as HumanAIController; - if (humanAI != null) + if (humanAI != null && !speaker.IsDead && !speaker.Removed) { if (prevSpeakerOrder != null) { @@ -255,7 +269,12 @@ namespace Barotrauma } else { - if (Options.Any()) + if (ShouldInterrupt()) + { + ResetSpeaker(); + interrupt = true; + } + else if (Options.Any()) { Options[selectedOption].Update(deltaTime); } @@ -335,6 +354,11 @@ namespace Barotrauma } } + if (targetCharacter != null && !string.IsNullOrWhiteSpace(InvokerTag)) + { + ParentEvent.AddTarget(InvokerTag, targetCharacter); + } + ShowDialog(speaker, targetCharacter); dialogOpened = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs index f198ac6f3..17ab504c6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/GiveSkillExpAction.cs @@ -42,7 +42,7 @@ namespace Barotrauma var targets = ParentEvent.GetTargets(TargetTag).Where(e => e is Character).Select(e => e as Character); foreach (var target in targets) { - target.Info?.IncreaseSkillLevel(Skill?.ToLowerInvariant(), Amount, target.WorldPosition + Vector2.UnitY * 150.0f); + target.Info?.IncreaseSkillLevel(Skill?.ToLowerInvariant(), Amount, target.Position + Vector2.UnitY * 150.0f); } isFinished = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs index 16b62c25b..35e6a5a62 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs @@ -109,14 +109,14 @@ namespace Barotrauma ISpatialEntity spawnPos = GetSpawnPos(); Entity.Spawner.AddToSpawnQueue(CharacterPrefab.HumanSpeciesName, OffsetSpawnPos(spawnPos?.WorldPosition ?? Vector2.Zero, 100.0f), onSpawn: newCharacter => { - newCharacter.TeamID = Character.TeamType.FriendlyNPC; + newCharacter.TeamID = CharacterTeamType.FriendlyNPC; newCharacter.EnableDespawn = false; humanPrefab.GiveItems(newCharacter, newCharacter.Submarine); if (LootingIsStealing) { - foreach (Item item in newCharacter.Inventory.Items) + foreach (Item item in newCharacter.Inventory.AllItems) { - if (item != null) { item.SpawnedInOutpost = true; } + item.SpawnedInOutpost = true; } } newCharacter.CharacterHealth.MaxVitality *= humanPrefab.HealthMultiplier; @@ -200,12 +200,18 @@ namespace Barotrauma } void onSpawned(Item newItem) { - if (!string.IsNullOrEmpty(TargetTag) && newItem != null) + if (newItem != null) { - ParentEvent.AddTarget(TargetTag, newItem); + if (!string.IsNullOrEmpty(TargetTag)) + { + ParentEvent.AddTarget(TargetTag, newItem); + } + if (IgnoreByAI) + { + newItem.AddTag("ignorebyai"); + } } spawnedEntity = newItem; - newItem?.SetIgnoreByAI(IgnoreByAI); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs index cefadd1d6..ceb0da293 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerAction.cs @@ -24,9 +24,12 @@ namespace Barotrauma [Serialize(0.0f, true, description: "Range both entities must be within to activate the trigger.")] public float Radius { get; set; } - [Serialize(true, true, description: "If true, characters who are being targeted by some enemy cannot trigger the event.")] + [Serialize(true, true, description: "If true, characters who are being targeted by some enemy cannot trigger the action.")] public bool DisableInCombat { get; set; } + [Serialize(true, true, description: "If true, dead/unconscious characters cannot trigger the action.")] + public bool DisableIfTargetIncapacitated { get; set; } + private float distance; public TriggerAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) @@ -59,6 +62,7 @@ namespace Barotrauma foreach (Entity e1 in targets1) { if (DisableInCombat && IsInCombat(e1)) { continue; } + if (DisableIfTargetIncapacitated && e1 is Character character1 && (character1.IsDead || character1.IsIncapacitated)) { continue; } if (!string.IsNullOrEmpty(TargetModuleType)) { if (IsCloseEnoughToHull(e1, out Hull hull)) @@ -75,6 +79,7 @@ namespace Barotrauma { if (e1 == e2) { continue; } if (DisableInCombat && IsInCombat(e2)) { continue; } + if (DisableIfTargetIncapacitated && e2 is Character character2 && (character2.IsDead || character2.IsIncapacitated)) { continue; } Vector2 pos1 = e1.WorldPosition; Vector2 pos2 = e2.WorldPosition; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index 98ff583d4..e02fb93a1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -3,6 +3,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; +using Barotrauma.Extensions; namespace Barotrauma { @@ -51,6 +52,12 @@ namespace Barotrauma private float roundDuration; + private bool isCrewAway; + //how long it takes after the crew returns for the event manager to resume normal operation + const float CrewAwayResetDelay = 60.0f; + private float crewAwayResetTimer; + private float crewAwayDuration; + private readonly List pendingEventSets = new List(); private readonly Dictionary> selectedEvents = new Dictionary>(); @@ -144,6 +151,9 @@ namespace Barotrauma PreloadContent(GetFilesToPreload()); roundDuration = 0.0f; + isCrewAway = false; + crewAwayDuration = 0.0f; + crewAwayResetTimer = 0.0f; intensityUpdateTimer = 0.0f; CalculateCurrentIntensity(0.0f); currentIntensity = targetIntensity; @@ -258,26 +268,23 @@ namespace Barotrauma var doc = characterPrefab.XDocument; var rootElement = doc.Root; var mainElement = rootElement.IsOverride() ? rootElement.FirstElement() : rootElement; - - foreach (var soundElement in mainElement.GetChildElements("sound")) - { - var sound = Submarine.LoadRoundSound(soundElement); - } - string speciesName = mainElement.GetAttributeString("speciesname", null); - if (string.IsNullOrWhiteSpace(speciesName)) - { - speciesName = mainElement.GetAttributeString("name", null); - if (!string.IsNullOrWhiteSpace(speciesName)) - { - DebugConsole.NewMessage($"Error in {file.Path}: 'name' is deprecated! Use 'speciesname' instead.", Color.Orange); - } - else - { - throw new Exception($"Species name null in {file.Path}"); - } - } - + mainElement.GetChildElements("sound").ForEach(e => Submarine.LoadRoundSound(e)); + if (!CharacterPrefab.CheckSpeciesName(mainElement, file.Path, out string speciesName)) { continue; } bool humanoid = mainElement.GetAttributeBool("humanoid", false); + CharacterPrefab originalCharacter; + if (characterPrefab.VariantOf != null) + { + originalCharacter = CharacterPrefab.FindBySpeciesName(characterPrefab.VariantOf); + var originalRoot = originalCharacter.XDocument.Root; + var originalMainElement = originalRoot.IsOverride() ? originalRoot.FirstElement() : originalRoot; + originalMainElement.GetChildElements("sound").ForEach(e => Submarine.LoadRoundSound(e)); + if (!CharacterPrefab.CheckSpeciesName(mainElement, file.Path, out string name)) { continue; } + speciesName = name; + if (mainElement.Attribute("humanoid") == null) + { + humanoid = originalMainElement.GetAttributeBool("humanoid", false); + } + } RagdollParams ragdollParams; if (humanoid) { @@ -335,13 +342,31 @@ namespace Barotrauma { if (level == null) { return; } int applyCount = 1; + List> spawnPosFilter = new List>(); if (eventSet.PerRuin) { applyCount = Level.Loaded.Ruins.Count(); + foreach (var ruin in Level.Loaded.Ruins) + { + spawnPosFilter.Add((Level.InterestingPosition pos) => { return pos.Ruin == ruin; }); + } + } + else if (eventSet.PerCave) + { + applyCount = Level.Loaded.Caves.Count(); + foreach (var cave in Level.Loaded.Caves) + { + spawnPosFilter.Add((Level.InterestingPosition pos) => { return pos.Cave == cave; }); + } } else if (eventSet.PerWreck) { - applyCount = Submarine.Loaded.Count(s => s.Info.IsWreck && (s.WreckAI == null || !s.WreckAI.IsAlive)); + var wrecks = Submarine.Loaded.Where(s => s.Info.IsWreck && (s.WreckAI == null || !s.WreckAI.IsAlive)); + applyCount = wrecks.Count(); + foreach (var wreck in wrecks) + { + spawnPosFilter.Add((Level.InterestingPosition pos) => { return pos.Submarine == wreck; }); + } } for (int i = 0; i < applyCount; i++) { @@ -358,6 +383,7 @@ namespace Barotrauma var newEvent = eventPrefab.First.CreateInstance(); if (newEvent == null) { continue; } newEvent.Init(true); + if (i < spawnPosFilter.Count) { newEvent.SpawnPosFilter = spawnPosFilter[i]; } DebugConsole.Log("Initialized event " + newEvent.ToString()); if (!selectedEvents.ContainsKey(eventSet)) { @@ -442,6 +468,14 @@ namespace Barotrauma } } + if (eventSet.DelayWhenCrewAway) + { + if ((isCrewAway && crewAwayDuration < settings.FreezeDurationWhenCrewAway) || crewAwayResetTimer > 0.0f) + { + return false; + } + } + if ((Submarine.MainSub == null || distanceTraveled < eventSet.MinDistanceTraveled) && roundDuration < eventSet.MinMissionTime) { @@ -493,6 +527,25 @@ namespace Barotrauma } } + if (IsCrewAway()) + { + isCrewAway = true; + crewAwayResetTimer = CrewAwayResetDelay; + crewAwayDuration += deltaTime; + } + else if (crewAwayResetTimer > 0.0f) + { + isCrewAway = false; + crewAwayResetTimer -= deltaTime; + } + else + { + isCrewAway = false; + crewAwayDuration = 0.0f; + eventThreshold += settings.EventThresholdIncrease * deltaTime; + eventCoolDown -= deltaTime; + } + calculateDistanceTraveledTimer -= deltaTime; if (calculateDistanceTraveledTimer <= 0.0f) { @@ -500,9 +553,6 @@ namespace Barotrauma calculateDistanceTraveledTimer = CalculateDistanceTraveledInterval; } - eventThreshold += settings.EventThresholdIncrease * deltaTime; - eventCoolDown -= deltaTime; - if (currentIntensity < eventThreshold) { bool recheck = false; @@ -526,7 +576,10 @@ namespace Barotrauma { activeEvents.Add(ev); eventThreshold = settings.DefaultEventThreshold; - eventCoolDown = settings.EventCooldown; + if (eventSet.TriggerEventCooldown && selectedEvents[eventSet].Any(e => e.Prefab.TriggerEventCooldown)) + { + eventCoolDown = settings.EventCooldown; + } } } @@ -563,7 +616,7 @@ namespace Barotrauma int characterCount = 0; foreach (Character character in Character.CharacterList) { - if (character.IsDead || character.TeamID == Character.TeamType.FriendlyNPC) { continue; } + if (character.IsDead || character.TeamID == CharacterTeamType.FriendlyNPC) { continue; } if (character.AIController is HumanAIController || character.IsRemotePlayer) { avgCrewHealth += character.Vitality / character.MaxVitality * (character.IsUnconscious ? 0.5f : 1.0f); @@ -586,9 +639,8 @@ namespace Barotrauma { if (character.IsDead || character.IsIncapacitated || !character.Enabled || character.IsPet || character.Params.CompareGroup("human")) { continue; } - EnemyAIController enemyAI = character.AIController as EnemyAIController; - if (enemyAI == null) continue; - + if (!(character.AIController is EnemyAIController enemyAI)) { continue; } + if (character.CurrentHull?.Submarine != null && (character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine))) { @@ -681,7 +733,6 @@ namespace Barotrauma } } - /// /// Finds all actions in a ScriptedEvent /// @@ -750,5 +801,74 @@ namespace Barotrauma #endif return refEntity; } + + private bool IsCrewAway() + { +#if CLIENT + return Character.Controlled != null && IsCharacterAway(Character.Controlled); +#else + int playerCount = 0; + int awayPlayerCount = 0; + foreach (Barotrauma.Networking.Client client in GameMain.Server.ConnectedClients) + { + if (client.Character == null || client.Character.IsDead || client.Character.IsIncapacitated) { continue; } + + playerCount++; + if (IsCharacterAway(client.Character)) { awayPlayerCount++; } + } + return playerCount > 0 && awayPlayerCount / (float)playerCount > 0.5f; +#endif + } + + private bool IsCharacterAway(Character character) + { + if (character.Submarine != null) + { + switch (character.Submarine.Info.Type) + { + case SubmarineType.Player: + case SubmarineType.Outpost: + case SubmarineType.OutpostModule: + return false; + case SubmarineType.Wreck: + case SubmarineType.BeaconStation: + return true; + } + } + + const int maxDist = 1000; + + if (Level.Loaded != null) + { + foreach (var ruin in Level.Loaded.Ruins) + { + Rectangle area = ruin.Area; + area.Inflate(maxDist, maxDist); + if (area.Contains(character.WorldPosition)) { return true; } + } + foreach (var cave in Level.Loaded.Caves) + { + Rectangle area = cave.Area; + area.Inflate(maxDist, maxDist); + if (area.Contains(character.WorldPosition)) { return true; } + } + } + + foreach (Submarine sub in Submarine.Loaded) + { + if (sub.Info.Type != SubmarineType.BeaconStation && sub.Info.Type != SubmarineType.Wreck) { continue; } + Rectangle worldBorders = new Rectangle( + sub.Borders.X + (int)sub.WorldPosition.X - maxDist, + sub.Borders.Y + (int)sub.WorldPosition.Y + maxDist, + sub.Borders.Width + maxDist * 2, + sub.Borders.Height + maxDist * 2); + if (Submarine.RectContains(worldBorders, character.WorldPosition)) + { + return true; + } + } + + return false; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManagerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManagerSettings.cs index 1f425f4ff..3572f831d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManagerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManagerSettings.cs @@ -24,6 +24,8 @@ namespace Barotrauma public readonly float MinLevelDifficulty = 0.0f; public readonly float MaxLevelDifficulty = 100.0f; + public readonly float FreezeDurationWhenCrewAway = 60.0f * 10.0f; + public static void Init() { List.Clear(); @@ -77,6 +79,8 @@ namespace Barotrauma MinLevelDifficulty = element.GetAttributeFloat("MinLevelDifficulty", 0.0f); MaxLevelDifficulty = element.GetAttributeFloat("MaxLevelDifficulty", 100.0f); + + FreezeDurationWhenCrewAway = element.GetAttributeFloat("FreezeDurationWhenCrewAway", 10.0f * 60.0f); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs index e0fc0e179..7727330e4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs @@ -10,6 +10,7 @@ namespace Barotrauma public readonly Type EventType; public readonly string MusicType; public readonly float SpawnProbability; + public readonly bool TriggerEventCooldown; public float Commonness; public string Identifier; @@ -35,6 +36,7 @@ namespace Barotrauma Identifier = ConfigElement.GetAttributeString("identifier", string.Empty); Commonness = element.GetAttributeFloat("commonness", 1.0f); SpawnProbability = Math.Clamp(element.GetAttributeFloat("spawnprobability", 1.0f), 0, 1); + TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", true); } public Event CreateInstance() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs index b93c047ac..050b131e5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs @@ -83,11 +83,14 @@ namespace Barotrauma public readonly bool IgnoreCoolDown; - public readonly bool PerRuin; - public readonly bool PerWreck; + public readonly bool PerRuin, PerCave, PerWreck; public readonly bool OncePerOutpost; + public readonly bool DelayWhenCrewAway; + + public readonly bool TriggerEventCooldown; + public readonly Dictionary Commonness; //Pair.First: event prefab, Pair.Second: commonness @@ -133,10 +136,13 @@ namespace Barotrauma MinMissionTime = element.GetAttributeFloat("minmissiontime", 0.0f); AllowAtStart = element.GetAttributeBool("allowatstart", false); - IgnoreCoolDown = element.GetAttributeBool("ignorecooldown", parentSet?.IgnoreCoolDown ?? false); PerRuin = element.GetAttributeBool("perruin", false); + PerCave = element.GetAttributeBool("percave", false); PerWreck = element.GetAttributeBool("perwreck", false); + IgnoreCoolDown = element.GetAttributeBool("ignorecooldown", parentSet?.IgnoreCoolDown ?? (PerRuin || PerCave || PerWreck)); + DelayWhenCrewAway = element.GetAttributeBool("delaywhencrewaway", !PerRuin && !PerCave && !PerWreck); OncePerOutpost = element.GetAttributeBool("perwreck", false); + TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", true); Commonness[""] = 1.0f; foreach (XElement subElement in element.Elements()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs index 2a1ae5069..b7fa8a6a9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs @@ -94,7 +94,10 @@ namespace Barotrauma cargoSpawnPos.Position.X + Rand.Range(-20.0f, 20.0f, Rand.RandSync.Server), cargoRoom.Rect.Y - cargoRoom.Rect.Height + itemPrefab.Size.Y / 2); - var item = new Item(itemPrefab, position, cargoRoom.Submarine); + var item = new Item(itemPrefab, position, cargoRoom.Submarine) + { + SpawnedInOutpost = true + }; item.FindHull(); items.Add(item); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs index efcc1c6ba..0362e10de 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs @@ -16,11 +16,11 @@ namespace Barotrauma get { return false; } } - private Character.TeamType Winner + private CharacterTeamType Winner { get { - if (GameMain.GameSession?.WinningTeam == null) { return Character.TeamType.None; } + if (GameMain.GameSession?.WinningTeam == null) { return CharacterTeamType.None; } return GameMain.GameSession.WinningTeam.Value; } } @@ -29,14 +29,14 @@ namespace Barotrauma { get { - if (Winner == Character.TeamType.None || string.IsNullOrEmpty(base.SuccessMessage)) { return ""; } + if (Winner == CharacterTeamType.None || string.IsNullOrEmpty(base.SuccessMessage)) { return ""; } //disable success message for now if it hasn't been translated if (!TextManager.ContainsTag("MissionSuccess." + Prefab.TextIdentifier)) { return ""; } - var loser = Winner == Character.TeamType.Team1 ? - Character.TeamType.Team2 : - Character.TeamType.Team1; + var loser = Winner == CharacterTeamType.Team1 ? + CharacterTeamType.Team2 : + CharacterTeamType.Team1; return base.SuccessMessage .Replace("[loser]", GetTeamName(loser)) @@ -44,11 +44,6 @@ namespace Barotrauma } } - public override int TeamCount - { - get { return 2; } - } - public CombatMission(MissionPrefab prefab, Location[] locations) : base(prefab, locations) { @@ -74,13 +69,13 @@ namespace Barotrauma }; } - public static string GetTeamName(Character.TeamType teamID) + public static string GetTeamName(CharacterTeamType teamID) { - if (teamID == Character.TeamType.Team1) + if (teamID == CharacterTeamType.Team1) { return teamNames.Length > 0 ? teamNames[0] : "Team 1"; } - else if (teamID == Character.TeamType.Team2) + else if (teamID == CharacterTeamType.Team2) { return teamNames.Length > 1 ? teamNames[1] : "Team 2"; } @@ -91,7 +86,7 @@ namespace Barotrauma public bool IsInWinningTeam(Character character) { return character != null && - Winner != Character.TeamType.None && + Winner != CharacterTeamType.None && Winner == character.TeamID; } @@ -104,7 +99,7 @@ namespace Barotrauma } subs = new Submarine[] { Submarine.MainSubs[0], Submarine.MainSubs[1] }; - subs[0].TeamID = Character.TeamType.Team1; subs[1].TeamID = Character.TeamType.Team2; + subs[0].TeamID = CharacterTeamType.Team1; subs[1].TeamID = CharacterTeamType.Team2; subs[0].NeutralizeBallast(); subs[1].NeutralizeBallast(); subs[1].SetPosition(subs[1].FindSpawnPos(Level.Loaded.EndPosition)); subs[1].FlipX(); @@ -122,7 +117,7 @@ namespace Barotrauma { if (GameMain.NetworkMember == null) return; - if (Winner != Character.TeamType.None) + if (Winner != CharacterTeamType.None) { GiveReward(); completed = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs index 7a8e774e4..1aeb97ac3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs @@ -14,6 +14,8 @@ namespace Barotrauma private Dictionary RelevantLevelResources { get; } = new Dictionary(); private List> MissionClusterPositions { get; } = new List>(); + private readonly HashSet caves = new HashSet(); + public override IEnumerable SonarPositions { get @@ -74,6 +76,8 @@ namespace Barotrauma #endif } + caves.Clear(); + if (IsClient) { return; } foreach (var kvp in ResourceClusters) { @@ -93,6 +97,19 @@ namespace Barotrauma if (spawnedResources.None()) { continue; } SpawnedResources.Add(kvp.Key, spawnedResources); kvp.Value.Second = rotation; + + foreach (Level.Cave cave in Level.Loaded.Caves) + { + foreach (Item spawnedResource in spawnedResources) + { + if (cave.Area.Contains(spawnedResource.WorldPosition)) + { + cave.DisplayOnSonar = true; + caves.Add(cave); + break; + } + } + } } CalculateMissionClusterPositions(); FindRelevantLevelResources(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index 31d5a4d21..bcf370b21 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -85,11 +85,6 @@ namespace Barotrauma get { return true; } } - public virtual int TeamCount - { - get { return 1; } - } - public virtual IEnumerable SonarPositions { get { return Enumerable.Empty(); } @@ -184,11 +179,6 @@ namespace Barotrauma public virtual void Update(float deltaTime) { } - public virtual void AssignTeamIDs(List clients) - { - clients.ForEach(c => c.TeamID = Character.TeamType.Team1); - } - protected void ShowMessage(int missionState) { ShowMessageProjSpecific(missionState); @@ -238,7 +228,16 @@ namespace Barotrauma { if (GameMain.GameSession.GameMode is CampaignMode && !IsClient) { - int srcIndex = Locations[0].Type.Identifier.Equals(from, StringComparison.OrdinalIgnoreCase) ? 0 : 1; + int srcIndex = -1; + for (int i = 0; i < Locations.Length; i++) + { + if (Locations[i].Type.Identifier.Equals(from, StringComparison.OrdinalIgnoreCase)) + { + srcIndex = i; + break; + } + } + if (srcIndex == -1) { return; } var upgradeLocation = Locations[srcIndex]; upgradeLocation.ChangeType(LocationType.List.Find(lt => lt.Identifier.Equals(to, StringComparison.OrdinalIgnoreCase))); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs index 79088e8cb..e92abdbdb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs @@ -125,7 +125,7 @@ namespace Barotrauma foreach (var monster in monsters) { monster.Enabled = false; - if (monster.Params.AI.EnforceAggressiveBehaviorForMissions) + if (monster.Params.AI != null && monster.Params.AI.EnforceAggressiveBehaviorForMissions) { foreach (var targetParam in monster.Params.AI.Targets) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs index 331be9b33..6a8117551 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs @@ -20,7 +20,9 @@ namespace Barotrauma private readonly float itemSpawnRadius = 800.0f; private readonly float approachItemsRadius = 1000.0f; + private readonly float nestObjectRadius = 1000.0f; private readonly float monsterSpawnRadius = 3000.0f; + private readonly int nestObjectAmount = 10; private readonly bool requireDelivery; @@ -53,6 +55,9 @@ namespace Barotrauma approachItemsRadius = prefab.ConfigElement.GetAttributeFloat("approachitemsradius", itemSpawnRadius * 2.0f); monsterSpawnRadius = prefab.ConfigElement.GetAttributeFloat("monsterspawnradius", approachItemsRadius * 2.0f); + nestObjectRadius = prefab.ConfigElement.GetAttributeFloat("nestobjectradius", itemSpawnRadius * 2.0f); + nestObjectAmount = prefab.ConfigElement.GetAttributeInt("nestobjectamount", 10); + requireDelivery = prefab.ConfigElement.GetAttributeBool("requiredelivery", false); string spawnPositionTypeStr = prefab.ConfigElement.GetAttributeString("spawntype", ""); @@ -62,7 +67,6 @@ namespace Barotrauma spawnPositionType = Level.PositionType.Cave | Level.PositionType.Ruin; } - foreach (var monsterElement in prefab.ConfigElement.GetChildElements("monster")) { string speciesName = monsterElement.GetAttributeString("character", string.Empty); @@ -107,6 +111,25 @@ namespace Barotrauma List spawnEdges = new List(); if (spawnPositionType == Level.PositionType.Cave) { + Level.Cave closestCave = null; + float closestCaveDist = float.PositiveInfinity; + foreach (var cave in Level.Loaded.Caves) + { + float dist = Vector2.DistanceSquared(nestPosition, cave.Area.Center.ToVector2()); + if (dist < closestCaveDist) + { + closestCave = cave; + closestCaveDist = dist; + } + } + if (closestCave != null) + { + closestCave.DisplayOnSonar = true; + SpawnNestObjects(level, closestCave); +#if SERVER + selectedCave = closestCave; +#endif + } var nearbyCells = Level.Loaded.GetCells(nestPosition, searchDepth: 3); if (nearbyCells.Any()) { @@ -188,6 +211,11 @@ namespace Barotrauma } } + private void SpawnNestObjects(Level level, Level.Cave cave) + { + level.LevelObjectManager.PlaceNestObjects(level, cave, nestPosition, nestObjectRadius, nestObjectAmount); + } + public override void Update(float deltaTime) { if (IsClient) @@ -279,6 +307,10 @@ namespace Barotrauma { GiveReward(); completed = true; + if (completed) + { + ChangeLocationType("None", "Explored"); + } } foreach (Item item in items) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs index defadaf0a..8d90652d7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs @@ -1,4 +1,5 @@ using Barotrauma.Extensions; +using Barotrauma.Items.Components; using FarseerPhysics; using Microsoft.Xna.Framework; using System; @@ -168,10 +169,11 @@ namespace Barotrauma //try to find a container and place the item inside it if (!string.IsNullOrEmpty(containerTag) && item.ParentInventory == null) { + List validContainers = new List(); foreach (Item it in Item.ItemList) { if (!it.HasTag(containerTag)) { continue; } - if (it.NonInteractable) { continue; } + if (!it.IsPlayerTeamInteractable) { continue; } switch (spawnPositionType) { case Level.PositionType.Cave: @@ -185,15 +187,18 @@ namespace Barotrauma if (it.Submarine == null || it.Submarine.Info.Type != SubmarineType.Wreck) { continue; } break; } - var itemContainer = it.GetComponent(); - if (itemContainer == null) { continue; } - if (itemContainer.Combine(item, user: null)) + var itemContainer = it.GetComponent(); + if (itemContainer != null && itemContainer.Inventory.CanBePut(item)) { validContainers.Add(itemContainer); } + } + if (validContainers.Any()) + { + var selectedContainer = validContainers.GetRandom(); + if (selectedContainer.Combine(item, user: null)) { #if SERVER - originalInventoryID = it.ID; - originalItemContainerIndex = (byte)it.GetComponentIndex(itemContainer); + originalInventoryID = selectedContainer.Item.ID; + originalItemContainerIndex = (byte)selectedContainer.Item.GetComponentIndex(selectedContainer); #endif - break; } // Placement successful } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs index 007b6ddbf..1414a2f7b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs @@ -138,6 +138,11 @@ namespace Barotrauma var removals = new List(); foreach (var position in availablePositions) { + if (SpawnPosFilter != null && !SpawnPosFilter(position)) + { + removals.Add(position); + continue; + } if (position.Submarine != null) { if (position.Submarine.WreckAI != null && position.Submarine.WreckAI.IsAlive) @@ -180,33 +185,36 @@ 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); - var removedPositions = new List(); - foreach (var position in availablePositions) - { - if (Rand.Value(Rand.RandSync.Server) > prefab.SpawnProbability) - { - removedPositions.Add(position); - } - } - removedPositions.ForEach(p => availablePositions.Remove(p)); bool isSubOrWreck = spawnPosType == Level.PositionType.Ruin || spawnPosType == Level.PositionType.Wreck; if (affectSubImmediately && !isSubOrWreck) { if (availablePositions.None()) { //no suitable position found, disable the event + spawnPos = null; Finished(); return; } + Submarine refSub = GetReferenceSub(); + if (Submarine.MainSubs.Length == 2 && Submarine.MainSubs[1] != null) + { + refSub = Submarine.MainSubs.GetRandom(Rand.RandSync.Unsynced); + } float closestDist = float.PositiveInfinity; //find the closest spawnposition that isn't too close to any of the subs foreach (var position in availablePositions) { Vector2 pos = position.Position.ToVector2(); - Submarine refSub = GetReferenceSub(); float dist = Vector2.DistanceSquared(pos, refSub.WorldPosition); foreach (Submarine sub in Submarine.Loaded) { @@ -248,7 +256,7 @@ namespace Barotrauma { foreach (var position in availablePositions) { - float dist = Vector2.DistanceSquared(position.Position.ToVector2(), GetReferenceSub().WorldPosition); + float dist = Vector2.DistanceSquared(position.Position.ToVector2(), refSub.WorldPosition); if (dist < closestDist) { closestDist = dist; @@ -262,11 +270,20 @@ namespace Barotrauma if (!isSubOrWreck) { float minDistance = 20000; - availablePositions.RemoveAll(p => Vector2.DistanceSquared(GetReferenceSub().WorldPosition, p.Position.ToVector2()) < minDistance * minDistance); + var refSub = GetReferenceSub(); + availablePositions.RemoveAll(p => Vector2.DistanceSquared(refSub.WorldPosition, p.Position.ToVector2()) < minDistance * minDistance); + if (Submarine.MainSubs.Length > 1) + { + for (int i = 1; i < Submarine.MainSubs.Length; i++) + { + availablePositions.RemoveAll(p => Vector2.DistanceSquared(Submarine.MainSubs[i].WorldPosition, p.Position.ToVector2()) < minDistance * minDistance); + } + } } if (availablePositions.None()) { //no suitable position found, disable the event + spawnPos = null; Finished(); return; } @@ -335,6 +352,8 @@ namespace Barotrauma if (spawnPos == null) { FindSpawnPosition(affectSubImmediately: true); + //the event gets marked as finished if a spawn point is not found + if (isFinished) { return; } spawnPending = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ForbiddenWordFilter.cs b/Barotrauma/BarotraumaShared/SharedSource/ForbiddenWordFilter.cs index 004bb2a65..593283d23 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ForbiddenWordFilter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ForbiddenWordFilter.cs @@ -14,7 +14,7 @@ namespace Barotrauma { try { - forbiddenWords = File.ReadAllLines(fileListPath).ToHashSet(); + forbiddenWords = File.ReadAllLines(fileListPath).Select(s => s.ToLowerInvariant()).ToHashSet(); } catch (IOException e) { @@ -42,16 +42,28 @@ namespace Barotrauma { foreach (string word in text.Split(delimiter)) { - words.Add(word); + words.Add(word.ToLowerInvariant()); } } - foreach (string word in words) + text = text.ToLowerInvariant(); + foreach (string forbidden in forbiddenWords) { - if (forbiddenWords.Any(w => Homoglyphs.Compare(word, w))) + if (forbidden.Contains(' ')) { - forbiddenWord = word; - return true; + if (words.Contains(forbidden.Trim())) + { + forbiddenWord = forbidden.Trim(); + return true; + } + } + else + { + if (text.Contains(forbidden)) + { + forbiddenWord = forbidden.Trim(); + return true; + } } } return false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs index 089d9f5b0..5453f987a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs @@ -196,11 +196,12 @@ namespace Barotrauma int amount = Rand.Range(validContainer.Value.MinAmount, validContainer.Value.MaxAmount + 1, Rand.RandSync.Server); for (int i = 0; i < amount; i++) { - if (validContainer.Key.Inventory.IsFull()) + if (validContainer.Key.Inventory.IsFull(takeStacksIntoAccount: true)) { containers.Remove(validContainer.Key); break; } + if (!validContainer.Key.Inventory.CanBePut(itemPrefab)) { break; } var item = new Item(itemPrefab, validContainer.Key.Item.Position, validContainer.Key.Item.Submarine) { SpawnedInOutpost = validContainer.Key.Item.SpawnedInOutpost, diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs index d231abb53..e8cb4f33a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs @@ -103,6 +103,10 @@ namespace Barotrauma public void PurchaseItems(List itemsToPurchase, bool removeFromCrate) { + // Check all the prices before starting the transaction + // to make sure the modifiers stay the same for the whole transaction + Dictionary buyValues = GetBuyValuesAtCurrentLocation(itemsToPurchase.Select(i => i.ItemPrefab)); + foreach (PurchasedItem item in itemsToPurchase) { // Add to the purchased items @@ -118,7 +122,7 @@ namespace Barotrauma } // Exchange money - var itemValue = GetBuyValueAtCurrentLocation(item); + var itemValue = item.Quantity * buyValues[item.ItemPrefab]; campaign.Money -= itemValue; Location.StoreCurrentBalance += itemValue; @@ -136,11 +140,35 @@ namespace Barotrauma OnPurchasedItemsChanged?.Invoke(); } - public int GetBuyValueAtCurrentLocation(PurchasedItem item) => item?.ItemPrefab != null && Location != null ? - item.Quantity * Location.GetAdjustedItemBuyPrice(item.ItemPrefab) : 0; + public Dictionary GetBuyValuesAtCurrentLocation(IEnumerable items) + { + var buyValues = new Dictionary(); + foreach (var item in items) + { + if (item == null) { continue; } + if (!buyValues.ContainsKey(item)) + { + var buyValue = Location?.GetAdjustedItemBuyPrice(item) ?? 0; + buyValues.Add(item, buyValue); + } + } + return buyValues; + } - public int GetSellValueAtCurrentLocation(ItemPrefab itemPrefab, int quantity = 1) => itemPrefab != null && Location != null ? - quantity * Location.GetAdjustedItemSellPrice(itemPrefab) : 0; + public Dictionary GetSellValuesAtCurrentLocation(IEnumerable items) + { + var sellValues = new Dictionary(); + foreach (var item in items) + { + if (item == null) { continue; } + if (!sellValues.ContainsKey(item)) + { + var sellValue = Location?.GetAdjustedItemSellPrice(item) ?? 0; + sellValues.Add(item, sellValue); + } + } + return sellValues; + } public void CreatePurchasedItems() { @@ -172,73 +200,59 @@ namespace Barotrauma #else foreach (Client client in GameMain.Server.ConnectedClients) { - ChatMessage msg = ChatMessage.Create("", $"CargoSpawnNotification~[roomname]=§{cargoRoom.RoomName}", ChatMessageType.ServerMessageBoxInGame, null); + ChatMessage msg = ChatMessage.Create("", + TextManager.ContainsTag(cargoRoom.RoomName) ? $"CargoSpawnNotification~[roomname]=§{cargoRoom.RoomName}" : $"CargoSpawnNotification~[roomname]={cargoRoom.RoomName}", + ChatMessageType.ServerMessageBoxInGame, null); msg.IconStyle = "StoreShoppingCrateIcon"; GameMain.Server.SendDirectChatMessage(msg, client); } #endif - Dictionary availableContainers = new Dictionary(); + List availableContainers = new List(); ItemPrefab containerPrefab = null; foreach (PurchasedItem pi in itemsToSpawn) { - float floorPos = cargoRoom.Rect.Y - cargoRoom.Rect.Height; + Vector2 position = GetCargoPos(cargoRoom, pi.ItemPrefab); - Vector2 position = new Vector2( - cargoRoom.Rect.Width > 40 ? Rand.Range(cargoRoom.Rect.X + 20, cargoRoom.Rect.Right - 20) : cargoRoom.Rect.Center.X, - floorPos); - - //check where the actual floor structure is in case the bottom of the hull extends below it - if (Submarine.PickBody( - ConvertUnits.ToSimUnits(new Vector2(position.X, cargoRoom.Rect.Y - cargoRoom.Rect.Height / 2)), - ConvertUnits.ToSimUnits(position), - collisionCategory: Physics.CollisionWall) != null) - { - float floorStructurePos = ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition.Y); - if (floorStructurePos > floorPos) - { - floorPos = floorStructurePos; - } - } - position.Y = floorPos + pi.ItemPrefab.Size.Y / 2; - - ItemContainer itemContainer = null; - if (!string.IsNullOrEmpty(pi.ItemPrefab.CargoContainerIdentifier)) - { - itemContainer = availableContainers.Keys.ToList().Find(ac => - ac.Item.Prefab.Identifier == pi.ItemPrefab.CargoContainerIdentifier || - ac.Item.Prefab.Tags.Contains(pi.ItemPrefab.CargoContainerIdentifier.ToLowerInvariant())); - - if (itemContainer == null) - { - containerPrefab = ItemPrefab.Prefabs.Find(ep => - ep.Identifier == pi.ItemPrefab.CargoContainerIdentifier || - (ep.Tags != null && ep.Tags.Contains(pi.ItemPrefab.CargoContainerIdentifier.ToLowerInvariant()))); - - if (containerPrefab == null) - { - DebugConsole.ThrowError("Cargo spawning failed - could not find the item prefab for container \"" + pi.ItemPrefab.CargoContainerIdentifier + "\"!"); - continue; - } - - Item containerItem = new Item(containerPrefab, position, wp.Submarine); - itemContainer = containerItem.GetComponent(); - if (itemContainer == null) - { - DebugConsole.ThrowError("Cargo spawning failed - container \"" + containerItem.Name + "\" does not have an ItemContainer component!"); - continue; - } - availableContainers.Add(itemContainer, itemContainer.Capacity); -#if SERVER - if (GameMain.Server != null) - { - Entity.Spawner.CreateNetworkEvent(itemContainer.Item, false); - } -#endif - } - } for (int i = 0; i < pi.Quantity; i++) { + ItemContainer itemContainer = null; + if (!string.IsNullOrEmpty(pi.ItemPrefab.CargoContainerIdentifier)) + { + itemContainer = availableContainers.Find(ac => + ac.Inventory.CanBePut(pi.ItemPrefab) && + (ac.Item.Prefab.Identifier == pi.ItemPrefab.CargoContainerIdentifier || + ac.Item.Prefab.Tags.Contains(pi.ItemPrefab.CargoContainerIdentifier.ToLowerInvariant()))); + + if (itemContainer == null) + { + containerPrefab = ItemPrefab.Prefabs.Find(ep => + ep.Identifier == pi.ItemPrefab.CargoContainerIdentifier || + (ep.Tags != null && ep.Tags.Contains(pi.ItemPrefab.CargoContainerIdentifier.ToLowerInvariant()))); + + if (containerPrefab == null) + { + DebugConsole.ThrowError("Cargo spawning failed - could not find the item prefab for container \"" + pi.ItemPrefab.CargoContainerIdentifier + "\"!"); + continue; + } + + Item containerItem = new Item(containerPrefab, position, wp.Submarine); + itemContainer = containerItem.GetComponent(); + if (itemContainer == null) + { + DebugConsole.ThrowError("Cargo spawning failed - container \"" + containerItem.Name + "\" does not have an ItemContainer component!"); + continue; + } + availableContainers.Add(itemContainer); + #if SERVER + if (GameMain.Server != null) + { + Entity.Spawner.CreateNetworkEvent(itemContainer.Item, false); + } + #endif + } + } + if (itemContainer == null) { //no container, place at the waypoint @@ -253,20 +267,6 @@ namespace Barotrauma } continue; } - //if the intial container has been removed due to it running out of space, add a new container - //of the same type and begin filling it - if (!availableContainers.ContainsKey(itemContainer)) - { - Item containerItemOverFlow = new Item(containerPrefab, position, wp.Submarine); - itemContainer = containerItemOverFlow.GetComponent(); - availableContainers.Add(itemContainer, itemContainer.Capacity); -#if SERVER - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) - { - Entity.Spawner.CreateNetworkEvent(itemContainer.Item, false); - } -#endif - } //place in the container if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) @@ -290,23 +290,38 @@ namespace Barotrauma wifiComponent.TeamID = sub.TeamID; } } - } - - //reduce the number of available slots in the container - //if there is a container - if (availableContainers.ContainsKey(itemContainer)) - { - availableContainers[itemContainer]--; - } - if (availableContainers.ContainsKey(itemContainer) && availableContainers[itemContainer] <= 0) - { - availableContainers.Remove(itemContainer); - } + } } } itemsToSpawn.Clear(); } + public static Vector2 GetCargoPos(Hull hull, ItemPrefab itemPrefab) + { + float floorPos = hull.Rect.Y - hull.Rect.Height; + + Vector2 position = new Vector2( + hull.Rect.Width > 40 ? Rand.Range(hull.Rect.X + 20, hull.Rect.Right - 20) : hull.Rect.Center.X, + floorPos); + + //check where the actual floor structure is in case the bottom of the hull extends below it + if (Submarine.PickBody( + ConvertUnits.ToSimUnits(new Vector2(position.X, hull.Rect.Y - hull.Rect.Height / 2)), + ConvertUnits.ToSimUnits(position), + collisionCategory: Physics.CollisionWall) != null) + { + float floorStructurePos = ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition.Y); + if (floorStructurePos > floorPos) + { + floorPos = floorStructurePos; + } + } + + position.Y = floorPos + itemPrefab.Size.Y / 2; + + return position; + } + public void SavePurchasedItems(XElement parentElement) { var itemsElement = new XElement("cargo"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs index e236209b0..9defaf5b9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs @@ -48,20 +48,43 @@ namespace Barotrauma return false; } - Pair existingOrder = - ActiveOrders.Find(o => o.First.Prefab == order.Prefab && o.First.TargetEntity == order.TargetEntity && - (o.First.TargetType != Order.OrderTargetType.WallSection || o.First.WallSectionIndex == order.WallSectionIndex)); - + // Ignore orders work a bit differently since the "unignore" order counters the "ignore" order + var isUnignoreOrder = order.Identifier == "unignorethis"; + var orderPrefab = !isUnignoreOrder ? order.Prefab : Order.GetPrefab("ignorethis"); + Pair existingOrder = ActiveOrders.Find(o => + o.First.Prefab == orderPrefab && MatchesTarget(o.First.TargetEntity, order.TargetEntity) && + (o.First.TargetType != Order.OrderTargetType.WallSection || o.First.WallSectionIndex == order.WallSectionIndex)); + if (existingOrder != null) { - existingOrder.Second = fadeOutTime; - return false; + if (!isUnignoreOrder) + { + existingOrder.Second = fadeOutTime; + return false; + } + else + { + ActiveOrders.Remove(existingOrder); + return true; + } } - else + else if (!isUnignoreOrder) { ActiveOrders.Add(new Pair(order, fadeOutTime)); return true; } + + bool MatchesTarget(Entity existingTarget, Entity newTarget) + { + if (existingTarget == newTarget) { return true; } + if (existingTarget is Hull existingHullTarget && newTarget is Hull newHullTarget) + { + return existingHullTarget.linkedTo.Contains(newHullTarget); + } + return false; + } + + return false; } public void AddCharacterElements(XElement element) @@ -124,12 +147,14 @@ namespace Barotrauma AddCharacterToCrewList(character); AddCurrentOrderIcon(character, character.CurrentOrder, character.CurrentOrderOption); #endif - var idleObjective = character.AIController?.ObjectiveManager?.GetObjective(); - if (idleObjective != null) + if (character.AIController is HumanAIController humanAI) { - idleObjective.Behavior = character.Info.Job.Prefab.IdleBehavior; - } - + var idleObjective = humanAI.ObjectiveManager.GetObjective(); + if (idleObjective != null) + { + idleObjective.Behavior = character.Info.Job.Prefab.IdleBehavior; + } + } } public void AddCharacterInfo(CharacterInfo characterInfo) @@ -177,7 +202,7 @@ namespace Barotrauma for (int i = 0; i < spawnWaypoints.Count; i++) { var info = characterInfos[i]; - info.TeamID = Character.TeamType.Team1; + info.TeamID = CharacterTeamType.Team1; Character character = Character.Create(info, spawnWaypoints[i].WorldPosition, info.Name); if (character.Info != null) { @@ -262,8 +287,8 @@ namespace Barotrauma { foreach (Character npc in Character.CharacterList) { - if (npc.TeamID != Character.TeamType.FriendlyNPC || npc.CurrentHull == null || npc.IsIncapacitated) { continue; } - if (npc.AIController?.ObjectiveManager != null && (npc.AIController.ObjectiveManager.IsCurrentObjective() || npc.AIController.ObjectiveManager.IsCurrentObjective())) + if (npc.TeamID != CharacterTeamType.FriendlyNPC || npc.CurrentHull == null || npc.IsIncapacitated) { continue; } + if (npc.AIController is HumanAIController humanAI && (humanAI.ObjectiveManager.IsCurrentObjective() || humanAI.ObjectiveManager.IsCurrentObjective())) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs index 08735675c..ed3995e75 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs @@ -7,6 +7,7 @@ namespace Barotrauma public const float HostileThreshold = 0.1f; public const float ReputationLossPerNPCDamage = 0.1f; public const float ReputationLossPerStolenItemPrice = 0.01f; + public const float ReputationLossPerWallDamage = 0.1f; public const float MinReputationLossPerStolenItem = 0.5f; public const float MaxReputationLossPerStolenItem = 10.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index f4070f5df..8b32f255d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -11,8 +11,7 @@ namespace Barotrauma abstract partial class CampaignMode : GameMode { const int MaxMoney = int.MaxValue / 2; //about 1 billion - const int InitialMoney = 2500; - public const int MaxInitialSubmarinePrice = 6000; + public const int InitialMoney = 8500; //duration of the cinematic + credits at the end of the campaign protected const float EndCinematicDuration = 240.0f; @@ -700,7 +699,7 @@ namespace Barotrauma public void OutpostNPCAttacked(Character npc, Character attacker, AttackResult attackResult) { if (npc == null || attacker == null || npc.IsDead || npc.IsInstigator) { return; } - if (npc.TeamID != Character.TeamType.FriendlyNPC) { return; } + if (npc.TeamID != CharacterTeamType.FriendlyNPC) { return; } if (!attacker.IsRemotePlayer && attacker != Character.Controlled) { return; } Location location = Map?.CurrentLocation; if (location != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs index 37039ee37..608668d29 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -8,6 +8,8 @@ namespace Barotrauma { partial class MultiPlayerCampaign : CampaignMode { + public const int MinimumInitialMoney = 500; + private UInt16 lastUpdateID; public UInt16 LastUpdateID { @@ -57,7 +59,7 @@ namespace Barotrauma InitCampaignData(); } - public static MultiPlayerCampaign StartNew(string mapSeed) + public static MultiPlayerCampaign StartNew(string mapSeed, SubmarineInfo selectedSub) { MultiPlayerCampaign campaign = new MultiPlayerCampaign(); //only the server generates the map, the clients load it from a save file @@ -96,6 +98,9 @@ namespace Barotrauma private void Load(XElement element) { Money = element.GetAttributeInt("money", 0); + PurchasedLostShuttles = element.GetAttributeBool("purchasedlostshuttles", false); + PurchasedHullRepairs = element.GetAttributeBool("purchasedhullrepairs", false); + PurchasedItemRepairs = element.GetAttributeBool("purchaseditemrepairs", false); CheatsEnabled = element.GetAttributeBool("cheatsenabled", false); if (CheatsEnabled) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/PvPMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/PvPMode.cs index 4b3ecdc8d..9a7af996c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/PvPMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/PvPMode.cs @@ -1,4 +1,7 @@ +using Barotrauma.Networking; using System; +using System.Collections.Generic; +using System.Linq; namespace Barotrauma { @@ -7,5 +10,42 @@ namespace Barotrauma public PvPMode(GameModePreset preset, MissionPrefab missionPrefab) : base(preset, ValidateMissionPrefab(missionPrefab, MissionPrefab.PvPMissionClasses)) { } public PvPMode(GameModePreset preset, MissionType missionType, string seed) : base(preset, ValidateMissionType(missionType, MissionPrefab.PvPMissionClasses), seed) { } + + public void AssignTeamIDs(IEnumerable clients) + { + int teamWeight = 0; + List randList = new List(clients); + for (int i = 0; i < randList.Count; i++) + { + if (randList[i].PreferredTeam == CharacterTeamType.Team1 || + randList[i].PreferredTeam == CharacterTeamType.Team2) + { + randList[i].TeamID = randList[i].PreferredTeam; + teamWeight += randList[i].PreferredTeam == CharacterTeamType.Team1 ? -1 : 1; + randList.RemoveAt(i); + i--; + } + } + for (int i = 0; i @@ -117,7 +117,7 @@ namespace Barotrauma : this(submarineInfo) { CrewManager = new CrewManager(gameModePreset != null && gameModePreset.IsSinglePlayer); - GameMode = InstantiateGameMode(gameModePreset, seed, missionPrefab: missionPrefab); + GameMode = InstantiateGameMode(gameModePreset, seed, submarineInfo, missionPrefab: missionPrefab); } /// @@ -158,7 +158,7 @@ namespace Barotrauma } } - private GameMode InstantiateGameMode(GameModePreset gameModePreset, string seed, MissionPrefab missionPrefab = null, MissionType missionType = MissionType.None) + private GameMode InstantiateGameMode(GameModePreset gameModePreset, string seed, SubmarineInfo selectedSub, MissionPrefab missionPrefab = null, MissionType missionType = MissionType.None) { if (gameModePreset.GameModeType == typeof(CoOpMode)) { @@ -174,12 +174,22 @@ namespace Barotrauma } else if (gameModePreset.GameModeType == typeof(MultiPlayerCampaign)) { - return MultiPlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8)); + var campaign = MultiPlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), selectedSub); + if (campaign != null && selectedSub != null) + { + campaign.Money = Math.Max(MultiPlayerCampaign.MinimumInitialMoney, campaign.Money - selectedSub.Price); + } + return campaign; } #if CLIENT else if (gameModePreset.GameModeType == typeof(SinglePlayerCampaign)) { - return SinglePlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8)); + var campaign = SinglePlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), selectedSub); + if (campaign != null && selectedSub != null) + { + campaign.Money = Math.Max(SinglePlayerCampaign.MinimumInitialMoney, campaign.Money - selectedSub.Price); + } + return campaign; } else if (gameModePreset.GameModeType == typeof(TutorialMode)) { @@ -230,7 +240,7 @@ namespace Barotrauma /// /// Switch to another submarine. The sub is loaded when the next round starts. /// - public void SwitchSubmarine(SubmarineInfo newSubmarine, int cost) + public SubmarineInfo SwitchSubmarine(SubmarineInfo newSubmarine, int cost) { if (!OwnedSubmarines.Any(s => s.Name == newSubmarine.Name)) { @@ -252,6 +262,7 @@ namespace Barotrauma Campaign.Money -= cost; ((CampaignMode)GameMode).PendingSubmarineSwitch = newSubmarine; + return newSubmarine; } public void PurchaseSubmarine(SubmarineInfo newSubmarine) @@ -306,7 +317,7 @@ namespace Barotrauma Submarine = Submarine.MainSub = new Submarine(SubmarineInfo); foreach (Submarine sub in Submarine.GetConnectedSubs()) { - sub.TeamID = Character.TeamType.Team1; + sub.TeamID = CharacterTeamType.Team1; foreach (Item item in Item.ItemList) { if (item.Submarine != sub) { continue; } @@ -316,7 +327,7 @@ namespace Barotrauma } } } - if (GameMode.Mission != null && GameMode.Mission.TeamCount > 1 && Submarine.MainSubs[1] == null) + if (GameMode is PvPMode && Submarine.MainSubs[1] == null) { Submarine.MainSubs[1] = new Submarine(SubmarineInfo, true); } @@ -438,6 +449,8 @@ namespace Barotrauma } } + GameMain.Config.RecentlyEncounteredCreatures.Clear(); + GameMain.GameScreen.Cam.Position = Character.Controlled?.WorldPosition ?? Submarine.MainSub.WorldPosition; RoundStartTime = Timing.TotalTime; GameMain.ResetFrameTime(); @@ -573,7 +586,7 @@ namespace Barotrauma if (GameMain.NetLobbyScreen != null) GameMain.NetLobbyScreen.OnRoundEnded(); TabMenu.OnRoundEnded(); - GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData as string == "ConversationAction"); + GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData as string == "ConversationAction" || ReadyCheck.IsReadyCheck(mb)); #endif SteamAchievementManager.OnRoundEnded(this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs index 75f401b7a..182a5ef40 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs @@ -531,7 +531,7 @@ namespace Barotrauma List levels = new List(); foreach (XElement subElement in elements) { - if (!category.CanBeApplied(subElement)) { continue; } + if (!category.CanBeApplied(subElement, prefab)) { continue; } foreach (XElement component in subElement.Elements()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs index b819a3763..99ce67c26 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using Microsoft.Xna.Framework; using Barotrauma.IO; using Barotrauma.Extensions; +using System.Diagnostics; #if CLIENT using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Graphics; @@ -50,6 +51,7 @@ namespace Barotrauma public bool DynamicRangeCompressionEnabled { get; set; } public bool VoipAttenuationEnabled { get; set; } public bool UseDirectionalVoiceChat { get; set; } + public bool DisableVoiceChatFilters { get; set; } public IList AudioDeviceNames; public IList CaptureDeviceNames; @@ -68,9 +70,12 @@ namespace Barotrauma public float NoiseGateThreshold { get; set; } + public bool UseLocalVoiceByDefault { get; set; } + #if CLIENT - private KeyOrMouse[] keyMapping; + public KeyOrMouse[] keyMapping; private KeyOrMouse[] inventoryKeyMapping; + public static Dictionary ConsoleKeybinds = new Dictionary(); #endif private WindowMode windowMode; @@ -123,6 +128,8 @@ namespace Barotrauma set { jobPreferences = value; } } + public CharacterTeamType TeamPreference { get; set; } + public bool AreJobPreferencesEqual(List> compareTo) { if (jobPreferences == null || compareTo == null) return false; @@ -154,6 +161,8 @@ namespace Barotrauma public bool EnableMouseLook { get; set; } = true; + public bool EnableRadialDistortion { get; set; } = true; + public bool CrewMenuOpen { get; set; } = true; public bool ChatOpen { get; set; } = true; @@ -282,6 +291,12 @@ namespace Barotrauma private set; } + public XElement ServerFilterElement + { + get; + private set; + } + public volatile bool SuppressModFolderWatcher; public volatile bool WaitingForAutoUpdate; @@ -349,22 +364,28 @@ namespace Barotrauma private List> backupModOrder; - public void SwapPackages(ContentPackage corePackage, List regularPackages) + public void BackUpModOrder() { - backupModOrder = new List>(); - backupModOrder.Add(new Tuple(CurrentCorePackage, true)); - for (int i=0;i> + { + new Tuple(CurrentCorePackage, true) + }; + for (int i = 0; i < ContentPackage.RegularPackages.Count; i++) { var p = ContentPackage.RegularPackages[i]; backupModOrder.Add(new Tuple(p, EnabledRegularPackages.Contains(p))); } + } + public void SwapPackages(ContentPackage corePackage, List regularPackages) + { List packagesToDisable = new List(); packagesToDisable.Add(CurrentCorePackage); - packagesToDisable.AddRange(EnabledRegularPackages.Where(p => p.HasMultiplayerIncompatibleContent)); + packagesToDisable.AddRange(enabledRegularPackages.Where(p => p.HasMultiplayerIncompatibleContent)); List packagesToEnable = new List(); packagesToEnable.Add(corePackage); - packagesToEnable.AddRange(regularPackages); + List regularPackagesToAdd = regularPackages.Where(p => p.HasMultiplayerIncompatibleContent).ToList(); + packagesToEnable.AddRange(regularPackagesToAdd); IEnumerable filesOfDisabledPkgs = packagesToDisable.SelectMany(p => p.Files); IEnumerable filesOfEnabledPkgs = packagesToEnable.SelectMany(p => p.Files); @@ -378,14 +399,18 @@ namespace Barotrauma Path.GetFullPath(f1.Path).CleanUpPath() == Path.GetFullPath(f2.Path).CleanUpPath())).ToList(); CurrentCorePackage = corePackage; - enabledRegularPackages.RemoveAll(p => p.HasMultiplayerIncompatibleContent); enabledRegularPackages.AddRange(regularPackages); + enabledRegularPackages.RemoveAll(p => p.HasMultiplayerIncompatibleContent); enabledRegularPackages.AddRange(regularPackagesToAdd); DisableContentPackageItems(filesToDisable); EnableContentPackageItems(filesToEnable); RefreshContentPackageItems(filesOfEnabledPkgs.Concat(filesToDisable)); - ContentPackage.SortContentPackages(p => -regularPackages.IndexOf(p)); + ContentPackage.SortContentPackages(p => -regularPackages.IndexOf(p), config: this); + +#if DEBUG + Debug.Assert(enabledRegularPackages.Count == enabledRegularPackages.Distinct().Count()); +#endif } public void RestoreBackupPackages() @@ -395,7 +420,7 @@ namespace Barotrauma SwapPackages( backupModOrder[0].Item1, backupModOrder.Skip(1).Where(p => p.Item2).Select(p => p.Item1).ToList()); - ContentPackage.SortContentPackages(p => backupModOrder.FindIndex(n => n.Item1 == p)); + ContentPackage.SortContentPackages(p => backupModOrder.FindIndex(n => n.Item1 == p), config: this); backupModOrder = null; } @@ -428,6 +453,8 @@ namespace Barotrauma public void SortContentPackages(bool refreshAll = false) { + var previousEnabledRegularPackages = enabledRegularPackages.ToList(); + for (int i = enabledRegularPackages.Count - 1; i >= 0; i--) { var package = enabledRegularPackages[i]; @@ -465,6 +492,7 @@ namespace Barotrauma var sortedSelected = enabledRegularPackages .OrderBy(p => -ContentPackage.RegularPackages.IndexOf(p)) .ToList(); + if (previousEnabledRegularPackages.SequenceEqual(sortedSelected)) { return; } enabledRegularPackages.Clear(); enabledRegularPackages.AddRange(sortedSelected); CharacterPrefab.Prefabs.SortAll(); @@ -698,10 +726,19 @@ namespace Barotrauma private const float MinHUDScale = 0.75f, MaxHUDScale = 1.25f; public static float HUDScale { get; set; } + private const float MinInventoryScale = 0.75f, MaxInventoryScale = 1.25f; public static float InventoryScale { get; set; } + private const float MinTextScale = 0.5f, MaxTextScale = 1.5f; + public static float TextScale { get; set; } + private bool textScaleDirty; + public List CompletedTutorialNames { get; private set; } + public HashSet EncounteredCreatures { get; private set; } = new HashSet(); + public HashSet KilledCreatures { get; private set; } = new HashSet(); + + public readonly HashSet RecentlyEncounteredCreatures = new HashSet(); public static bool VerboseLogging { get; set; } public static bool SaveDebugConsoleLogs { get; set; } @@ -729,10 +766,13 @@ namespace Barotrauma public bool ShowLanguageSelectionPrompt { get; set; } + public static bool ShowOffensiveServerPrompt { get; set; } + private bool showTutorialSkipWarning = true; public static bool EnableSubmarineAutoSave { get; set; } public static int MaximumAutoSaves { get; set; } + public static int AutoSaveIntervalSeconds { get; set; } public static Color SubEditorBackgroundColor { get; set; } public static int SubEditorMaxUndoBuffer { get; set; } @@ -778,7 +818,8 @@ namespace Barotrauma if (File.Exists(cpPath) && !ContentPackage.AllPackages.Any(cp => Path.GetFullPath(cp.Path).CleanUpPath() == cpPath)) { - ContentPackage.AddPackage(new ContentPackage(cpPath)); + var newPackage = new ContentPackage(cpPath); + if (!newPackage.IsCorrupt) { ContentPackage.AddPackage(newPackage); } } } break; @@ -830,7 +871,8 @@ namespace Barotrauma if (File.Exists(cpPath) && !ContentPackage.AllPackages.Any(cp => Path.GetFullPath(cp.Path).CleanUpPath() == cpPath)) { - ContentPackage.AddPackage(new ContentPackage(cpPath)); + var newPackage = new ContentPackage(cpPath); + if (!newPackage.IsCorrupt) { ContentPackage.AddPackage(newPackage); } } if (reselectCore) { AutoSelectCorePackage(null); } } @@ -869,6 +911,7 @@ namespace Barotrauma LoadAudioSettings(doc); #if CLIENT LoadControls(doc); + LoadSubEditorImages(doc); #endif if (loadContentPackages) { @@ -905,6 +948,7 @@ namespace Barotrauma new XAttribute("savedebugconsolelogs", SaveDebugConsoleLogs), new XAttribute("submarineautosave", EnableSubmarineAutoSave), new XAttribute("maxautosaves", MaximumAutoSaves), + new XAttribute("autosaveintervalseconds", AutoSaveIntervalSeconds), new XAttribute("subeditorbackground", XMLExtensions.ColorToString(SubEditorBackgroundColor)), new XAttribute("subeditorundobuffer", SubEditorMaxUndoBuffer), new XAttribute("enablesplashscreen", EnableSplashScreen), @@ -1010,6 +1054,11 @@ namespace Barotrauma jobPreferences.Add(jobElement); } gameplay.Add(jobPreferences); + + var teamPreference = new XElement("teampreference"); + teamPreference.Add(new XAttribute("team", TeamPreference.ToString())); + gameplay.Add(teamPreference); + doc.Root.Add(gameplay); var playerElement = new XElement("player", @@ -1079,6 +1128,7 @@ namespace Barotrauma LoadAudioSettings(doc); #if CLIENT LoadControls(doc); + LoadSubEditorImages(doc); #endif LoadContentPackages(doc); @@ -1101,8 +1151,21 @@ namespace Barotrauma CompletedTutorialNames.Add(element.GetAttributeString("name", "")); } } + XElement encounters = doc.Root.Element("encountered"); + if (encounters != null) + { + EncounteredCreatures = new HashSet(encounters.GetAttributeStringArray("creatures", new string[0], convertToLowerInvariant: true)); + } + XElement kills = doc.Root.Element("killed"); + if (kills != null) + { + KilledCreatures = new HashSet(kills.GetAttributeStringArray("creatures", new string[0], convertToLowerInvariant: true)); + } + + ServerFilterElement = doc.Root.Element("serverfilters"); UnsavedSettings = false; + textScaleDirty = false; return true; } @@ -1130,6 +1193,7 @@ namespace Barotrauma new XAttribute("submarineautosave", EnableSubmarineAutoSave), new XAttribute("subeditorundobuffer", SubEditorMaxUndoBuffer), new XAttribute("maxautosaves", MaximumAutoSaves), + new XAttribute("autosaveintervalseconds", AutoSaveIntervalSeconds), new XAttribute("subeditorbackground", XMLExtensions.ColorToString(SubEditorBackgroundColor)), new XAttribute("enablesplashscreen", EnableSplashScreen), new XAttribute("usesteammatchmaking", UseSteamMatchmaking), @@ -1139,6 +1203,7 @@ namespace Barotrauma new XAttribute("pauseonfocuslost", PauseOnFocusLost), new XAttribute("aimassistamount", aimAssistAmount), new XAttribute("enablemouselook", EnableMouseLook), + new XAttribute("radialdistortion", EnableRadialDistortion), new XAttribute("chatopen", ChatOpen), new XAttribute("crewmenuopen", CrewMenuOpen), new XAttribute("campaigndisclaimershown", CampaignDisclaimerShown), @@ -1209,7 +1274,8 @@ namespace Barotrauma new XAttribute("voicesetting", VoiceSetting), new XAttribute("audiooutputdevice", System.Xml.XmlConvert.EncodeName(AudioOutputDevice ?? "")), new XAttribute("voicecapturedevice", System.Xml.XmlConvert.EncodeName(VoiceCaptureDevice ?? "")), - new XAttribute("noisegatethreshold", NoiseGateThreshold)); + new XAttribute("noisegatethreshold", NoiseGateThreshold), + new XAttribute("uselocalvoicebydefault", UseLocalVoiceByDefault)); XElement gSettings = doc.Root.Element("graphicssettings"); if (gSettings == null) @@ -1224,7 +1290,8 @@ namespace Barotrauma new XAttribute("chromaticaberration", ChromaticAberrationEnabled), new XAttribute("losmode", LosMode), new XAttribute("hudscale", HUDScale), - new XAttribute("inventoryscale", InventoryScale)); + new XAttribute("inventoryscale", InventoryScale), + new XAttribute("textscale", TextScale)); XElement contentPackagesElement = new XElement("contentpackages"); @@ -1273,6 +1340,25 @@ namespace Barotrauma inventoryKeyMappingElement.Add(new XAttribute($"slot{i}", bind.MouseButton)); } } + + var debugconsoleKeyMappingElement = new XElement("debugconsolemapping"); + doc.Root.Add(debugconsoleKeyMappingElement); + foreach (var (key, command) in ConsoleKeybinds) + { + debugconsoleKeyMappingElement.Add(new XElement("Keybind", + new XAttribute("key", key.ToString()), + new XAttribute("command", command))); + } + + if (ServerFilterElement == null) + { + ShowOffensiveServerPrompt = true; + ServerFilterElement = new XElement("serverfilters"); + } + GameMain.ServerListScreen?.SaveServerFilters(ServerFilterElement); + doc.Root.Add(ServerFilterElement); + + SubEditorScreen.ImageManager.Save(doc.Root); #endif var gameplay = new XElement("gameplay"); @@ -1317,6 +1403,9 @@ namespace Barotrauma } doc.Root.Add(tutorialElement); + doc.Root.Add(new XElement("encountered", new XAttribute("creatures", string.Join(",", EncounteredCreatures).Trim().ToLowerInvariant()))); + doc.Root.Add(new XElement("killed", new XAttribute("creatures", string.Join(",", KilledCreatures).Trim().ToLowerInvariant()))); + System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings { Indent = true, @@ -1353,6 +1442,7 @@ namespace Barotrauma QuickStartSubmarineName = doc.Root.GetAttributeString("quickstartsub", QuickStartSubmarineName); EnableSubmarineAutoSave = doc.Root.GetAttributeBool("submarineautosave", true); MaximumAutoSaves = doc.Root.GetAttributeInt("maxautosaves", 8); + AutoSaveIntervalSeconds = doc.Root.GetAttributeInt("autosaveintervalseconds", 300); SubEditorBackgroundColor = doc.Root.GetAttributeColor("subeditorbackground", new Color(0.051f, 0.149f, 0.271f, 1.0f)); SubEditorMaxUndoBuffer = doc.Root.GetAttributeInt("subeditorundobuffer", 32); UseSteamMatchmaking = doc.Root.GetAttributeBool("usesteammatchmaking", UseSteamMatchmaking); @@ -1361,6 +1451,7 @@ namespace Barotrauma PauseOnFocusLost = doc.Root.GetAttributeBool("pauseonfocuslost", PauseOnFocusLost); AimAssistAmount = doc.Root.GetAttributeFloat("aimassistamount", AimAssistAmount); EnableMouseLook = doc.Root.GetAttributeBool("enablemouselook", EnableMouseLook); + EnableRadialDistortion = doc.Root.GetAttributeBool("radialdistortion", EnableRadialDistortion); CrewMenuOpen = doc.Root.GetAttributeBool("crewmenuopen", CrewMenuOpen); ChatOpen = doc.Root.GetAttributeBool("chatopen", ChatOpen); CorpseDespawnDelay = doc.Root.GetAttributeInt("corpsedespawndelay", 10 * 60); @@ -1389,6 +1480,12 @@ namespace Barotrauma jobPreferences.Add(new Pair(jobIdentifier, outfitVariant)); } } + + var teamPreferenceElement = gameplayElement.Element("teampreference"); + if (teamPreferenceElement != null) + { + TeamPreference = (CharacterTeamType)Enum.Parse(typeof(CharacterTeamType), teamPreferenceElement.GetAttributeString("team", CharacterTeamType.None.ToString())); + } } XElement playerElement = doc.Root.Element("player"); @@ -1430,6 +1527,7 @@ namespace Barotrauma ChromaticAberrationEnabled = graphicsSettings.GetAttributeBool("chromaticaberration", ChromaticAberrationEnabled); HUDScale = graphicsSettings.GetAttributeFloat("hudscale", HUDScale); InventoryScale = graphicsSettings.GetAttributeFloat("inventoryscale", InventoryScale); + TextScale = graphicsSettings.GetAttributeFloat("textscale", TextScale); var losModeStr = graphicsSettings.GetAttributeString("losmode", "Transparent"); if (!Enum.TryParse(losModeStr, out losMode)) { @@ -1466,6 +1564,7 @@ namespace Barotrauma VoiceCaptureDevice = System.Xml.XmlConvert.DecodeName(audioSettings.GetAttributeString("voicecapturedevice", VoiceCaptureDevice)); AudioOutputDevice = System.Xml.XmlConvert.DecodeName(audioSettings.GetAttributeString("audiooutputdevice", AudioOutputDevice)); NoiseGateThreshold = audioSettings.GetAttributeFloat("noisegatethreshold", NoiseGateThreshold); + UseLocalVoiceByDefault = audioSettings.GetAttributeBool("uselocalvoicebydefault", UseLocalVoiceByDefault); MicrophoneVolume = audioSettings.GetAttributeFloat("microphonevolume", MicrophoneVolume); string voiceSettingStr = audioSettings.GetAttributeString("voicesetting", ""); if (Enum.TryParse(voiceSettingStr, out VoiceMode voiceSetting)) @@ -1495,16 +1594,6 @@ namespace Barotrauma List subElements = regularElement?.Elements()?.ToList(); if (subElements != null) { - ContentPackage.SortContentPackages(p => - { - int index = subElements.FindIndex(e => - { - string name = e.GetAttributeString("name", null); - return p.Name.Equals(name, StringComparison.OrdinalIgnoreCase); - }); - return index; - }); - foreach (var subElement in subElements) { if (!bool.TryParse(subElement.GetAttributeString("enabled", "false"), out bool enabled) || !enabled) { continue; } @@ -1516,6 +1605,16 @@ namespace Barotrauma if (package == null) { continue; } enabledRegularPackages.Add(package); } + + ContentPackage.SortContentPackages(p => + { + int index = subElements.FindIndex(e => + { + string name = e.GetAttributeString("name", null); + return p.Name.Equals(name, StringComparison.OrdinalIgnoreCase); + }); + return index; + }, config: this); } } else @@ -1532,8 +1631,6 @@ namespace Barotrauma } } - ContentPackage.SortContentPackages(p => enabledContentPackagePaths.IndexOf(p.Path.CleanUpPath().ToLowerInvariant())); - foreach (string path in enabledContentPackagePaths) { ContentPackage package = ContentPackage.AllPackages @@ -1542,6 +1639,8 @@ namespace Barotrauma if (package.IsCorePackage) { CurrentCorePackage = package; } else { enabledRegularPackages.Add(package); } } + + ContentPackage.SortContentPackages(p => enabledContentPackagePaths.IndexOf(p.Path.CleanUpPath().ToLowerInvariant()), config: this); } if (CurrentCorePackage == null) @@ -1583,6 +1682,7 @@ namespace Barotrauma VoiceSetting = VoiceMode.Disabled; VoiceCaptureDevice = null; NoiseGateThreshold = -45; + UseLocalVoiceByDefault = false; windowMode = WindowMode.BorderlessWindowed; losMode = LosMode.Transparent; UseSteamMatchmaking = true; @@ -1597,6 +1697,7 @@ namespace Barotrauma CharacterRace = Race.White; aimAssistAmount = 0.5f; EnableMouseLook = true; + EnableRadialDistortion = true; CrewMenuOpen = true; ChatOpen = true; soundVolume = 0.5f; @@ -1622,6 +1723,8 @@ namespace Barotrauma VerboseLogging = false; SaveDebugConsoleLogs = false; AutoUpdateWorkshopItems = true; + TextScale = 1; + textScaleDirty = false; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/InputType.cs b/Barotrauma/BarotraumaShared/SharedSource/InputType.cs index 3c26158fa..fcac2eba3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/InputType.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/InputType.cs @@ -17,7 +17,9 @@ namespace Barotrauma Deselect, Shoot, Command, - ToggleInventory, + ToggleInventory, + TakeOneFromInventorySlot, + TakeHalfFromInventorySlot, NextFireMode, PreviousFireMode } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs index 8bd1a568c..3f055adb5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs @@ -14,7 +14,7 @@ namespace Barotrauma partial class CharacterInventory : Inventory { - private Character character; + private readonly Character character; public InvSlotType[] SlotTypes { @@ -33,8 +33,14 @@ namespace Barotrauma private set; } + private static string[] ParseSlotTypes(XElement element) + { + string slotString = element.GetAttributeString("slots", null); + return slotString == null ? new string[0] : slotString.Split(','); + } + public CharacterInventory(XElement element, Character character) - : base(character, element.GetAttributeString("slots", "").Split(',').Count()) + : base(character, ParseSlotTypes(element).Length) { this.character = character; IsEquipped = new bool[capacity]; @@ -42,7 +48,7 @@ namespace Barotrauma AccessibleWhenAlive = element.GetAttributeBool("accessiblewhenalive", true); - string[] slotTypeNames = element.GetAttributeString("slots", "").Split(','); + string[] slotTypeNames = ParseSlotTypes(element); System.Diagnostics.Debug.Assert(slotTypeNames.Length == capacity); for (int i = 0; i < capacity; i++) @@ -56,11 +62,9 @@ namespace Barotrauma SlotTypes[i] = parsedSlotType; switch (SlotTypes[i]) { - //case InvSlotType.Head: - //case InvSlotType.OuterClothes: case InvSlotType.LeftHand: case InvSlotType.RightHand: - hideEmptySlot[i] = true; + slots[i].HideIfEmpty = true; break; } } @@ -83,7 +87,7 @@ namespace Barotrauma continue; } - Entity.Spawner?.AddToSpawnQueue(itemPrefab, this); + Entity.Spawner?.AddToSpawnQueue(itemPrefab, this, ignoreLimbSlots: subElement.GetAttributeBool("forcetoslot", false)); } } @@ -91,18 +95,28 @@ namespace Barotrauma public int FindLimbSlot(InvSlotType limbSlot) { - for (int i = 0; i < Items.Length; i++) + for (int i = 0; i < slots.Length; i++) { if (SlotTypes[i] == limbSlot) { return i; } } return -1; } + public Item GetItemInLimbSlot(InvSlotType limbSlot) + { + for (int i = 0; i < slots.Length; i++) + { + if (SlotTypes[i] == limbSlot) { return slots[i].FirstOrDefault(); } + } + return null; + } + + public bool IsInLimbSlot(Item item, InvSlotType limbSlot) { - for (int i = 0; i < Items.Length; i++) + for (int i = 0; i < slots.Length; i++) { - if (Items[i] == item && SlotTypes[i] == limbSlot) { return true; } + if (SlotTypes[i] == limbSlot && slots[i].Contains(item)) { return true; } } return false; } @@ -118,9 +132,9 @@ namespace Barotrauma foreach (var allowedSlot in item.AllowedSlots) { InvSlotType slotsFree = InvSlotType.None; - for (int i = 0; i < Items.Length; i++) + for (int i = 0; i < slots.Length; i++) { - if (allowedSlot.HasFlag(SlotTypes[i]) && Items[i] == null) { slotsFree |= SlotTypes[i]; } + if (allowedSlot.HasFlag(SlotTypes[i]) && slots[i].Empty()) { slotsFree |= SlotTypes[i]; } } if (allowedSlot == slotsFree) { return true; } } @@ -130,7 +144,7 @@ namespace Barotrauma /// /// If there is no room in the generic inventory (InvSlotType.Any), check if the item can be auto-equipped into its respective limbslot /// - public bool TryPutItemWithAutoEquipCheck(Item item, Character user, List allowedSlots = null, bool createNetworkEvent = true) + public bool TryPutItemWithAutoEquipCheck(Item item, Character user, IEnumerable allowedSlots = null, bool createNetworkEvent = true) { // Does not auto-equip the item if specified and no suitable any slot found (for example handcuffs are not auto-equipped) if (item.AllowedSlots.Contains(InvSlotType.Any)) @@ -148,7 +162,7 @@ namespace Barotrauma /// /// If there is room, puts the item in the inventory and returns true, otherwise returns false /// - public override bool TryPutItem(Item item, Character user, List allowedSlots = null, bool createNetworkEvent = true) + public override bool TryPutItem(Item item, Character user, IEnumerable allowedSlots = null, bool createNetworkEvent = true) { if (allowedSlots == null || !allowedSlots.Any()) { return false; } if (item == null) @@ -174,7 +188,7 @@ namespace Barotrauma int currentSlot = -1; for (int i = 0; i < capacity; i++) { - if (Items[i] == item) + if (slots[i].Contains(item)) { currentSlot = i; if (allowedSlots.Any(a => a.HasFlag(SlotTypes[i]))) @@ -208,18 +222,18 @@ namespace Barotrauma bool free = true; for (int i = 0; i < capacity; i++) { - if (allowedSlot.HasFlag(SlotTypes[i]) && item.AllowedSlots.Any(s => s.HasFlag(SlotTypes[i])) && Items[i] != null && Items[i] != item) + if (allowedSlot.HasFlag(SlotTypes[i]) && item.AllowedSlots.Any(s => s.HasFlag(SlotTypes[i])) && slots[i].Items.Any(it => it != item)) { #if CLIENT if (PersonalSlots.HasFlag(SlotTypes[i])) { hidePersonalSlots = false; } #endif - if (!Items[i].AllowedSlots.Contains(InvSlotType.Any) || !TryPutItem(Items[i], character, new List { InvSlotType.Any }, true)) + if (!slots[i].First().AllowedSlots.Contains(InvSlotType.Any) || !TryPutItem(slots[i].FirstOrDefault(), character, new List { InvSlotType.Any }, true)) { free = false; #if CLIENT for (int j = 0; j < capacity; j++) { - if (slots != null && Items[j] == Items[i]) slots[j].ShowBorderHighlight(GUI.Style.Red, 0.1f, 0.9f); + if (visualSlots != null && slots[j] == slots[i]) { visualSlots[j].ShowBorderHighlight(GUI.Style.Red, 0.1f, 0.9f); } } #endif } @@ -230,7 +244,7 @@ namespace Barotrauma for (int i = 0; i < capacity; i++) { - if (allowedSlot.HasFlag(SlotTypes[i]) && item.AllowedSlots.Any(s => s.HasFlag(SlotTypes[i])) && Items[i] == null) + if (allowedSlot.HasFlag(SlotTypes[i]) && item.AllowedSlots.Any(s => s.HasFlag(SlotTypes[i])) && slots[i].Empty()) { #if CLIENT if (PersonalSlots.HasFlag(SlotTypes[i])) { hidePersonalSlots = false; } @@ -238,7 +252,7 @@ namespace Barotrauma bool removeFromOtherSlots = item.ParentInventory != this; if (placedInSlot == -1 && inWrongSlot) { - if (!hideEmptySlot[i] || SlotTypes[currentSlot] != InvSlotType.Any) removeFromOtherSlots = true; + if (!slots[i].HideIfEmpty || SlotTypes[currentSlot] != InvSlotType.Any) { removeFromOtherSlots = true; } } PutItem(item, i, user, removeFromOtherSlots, createNetworkEvent); @@ -254,35 +268,51 @@ namespace Barotrauma public int CheckIfAnySlotAvailable(Item item, bool inWrongSlot) { - for (int i = 0; i < capacity; i++) + //attempt to stack first + for (int i = 0; i < capacity; i++) + { + if (SlotTypes[i] != InvSlotType.Any) { continue; } + if (!slots[i].Empty() && CanBePut(item, i)) { - if (SlotTypes[i] != InvSlotType.Any) continue; - if (Items[i] == item) - { - return i; - } - } - for (int i = 0; i < capacity; i++) - { - if (SlotTypes[i] != InvSlotType.Any) continue; - if (inWrongSlot) - { - if (Items[i] != item && Items[i] != null) continue; - } - else - { - if (Items[i] != null) continue; - } - return i; } - + } + for (int i = 0; i < capacity; i++) + { + if (SlotTypes[i] != InvSlotType.Any) { continue; } + if (slots[i].Contains(item)) + { + return i; + } + } + for (int i = 0; i < capacity; i++) + { + if (SlotTypes[i] != InvSlotType.Any) { continue; } + if (CanBePut(item, i)) + { + return i; + } + } + for (int i = 0; i < capacity; i++) + { + if (SlotTypes[i] != InvSlotType.Any) { continue; } + if (inWrongSlot) + { + //another item already in the slot + if (slots[i].Any() && slots[i].Items.Any(it => it != item)) { continue; } + } + else + { + if (!CanBePut(item, i)) { continue; } + } + return i; + } return -1; } public override bool TryPutItem(Item item, int index, bool allowSwapping, bool allowCombine, Character user, bool createNetworkEvent = true) { - if (index < 0 || index >= Items.Length) + if (index < 0 || index >= slots.Length) { string errorMsg = "CharacterInventory.TryPutItem failed: index was out of range(" + index + ").\n" + Environment.StackTrace.CleanupStackTrace(); GameAnalyticsManager.AddErrorEventOnce("CharacterInventory.TryPutItem:IndexOutOfRange", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); @@ -292,18 +322,16 @@ namespace Barotrauma if (PersonalSlots.HasFlag(SlotTypes[index])) { hidePersonalSlots = false; } #endif //there's already an item in the slot - if (Items[index] != null) + if (slots[index].Any()) { - if (Items[index] == item) return false; - + if (slots[index].Contains(item)) { return false; } return base.TryPutItem(item, index, allowSwapping, allowCombine, user, createNetworkEvent); } if (SlotTypes[index] == InvSlotType.Any) { - if (!item.AllowedSlots.Contains(InvSlotType.Any)) return false; - if (Items[index] != null) return Items[index] == item; - + if (!item.AllowedSlots.Contains(InvSlotType.Any)) { return false; } + if (slots[index].Any()) { return slots[index].Contains(item); } PutItem(item, index, user, true, createNetworkEvent); return true; } @@ -311,28 +339,24 @@ namespace Barotrauma InvSlotType placeToSlots = InvSlotType.None; bool slotsFree = true; - List allowedSlots = item.AllowedSlots; - foreach (InvSlotType allowedSlot in allowedSlots) + foreach (InvSlotType allowedSlot in item.AllowedSlots) { - if (!allowedSlot.HasFlag(SlotTypes[index])) continue; + if (!allowedSlot.HasFlag(SlotTypes[index])) { continue; } #if CLIENT if (PersonalSlots.HasFlag(allowedSlot)) { hidePersonalSlots = false; } #endif for (int i = 0; i < capacity; i++) { - if (allowedSlot.HasFlag(SlotTypes[i]) && Items[i] != null && Items[i] != item) + if (allowedSlot.HasFlag(SlotTypes[i]) && slots[i].Any() && !slots[i].Contains(item)) { slotsFree = false; break; } - placeToSlots = allowedSlot; } } - - - if (!slotsFree) return false; + if (!slotsFree) { return false; } return TryPutItem(item, user, new List() { placeToSlots }, createNetworkEvent); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs index 34da543e7..41502725e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs @@ -13,7 +13,16 @@ namespace Barotrauma.Items.Components { partial class DockingPort : ItemComponent, IDrawableComponent, IServerSerializable { - private static List list = new List(); + public enum DirectionType + { + None, + Top, + Bottom, + Left, + Right + } + + private static readonly List list = new List(); public static IEnumerable List { get { return list; } @@ -37,7 +46,7 @@ namespace Barotrauma.Items.Components //force the sub to the correct position const float ForceLockDelay = 1.0f; - public int DockingDir { get; private set; } + public int DockingDir { get; set; } [Serialize("32.0,32.0", false, description: "How close the docking port has to be to another port to dock.")] public Vector2 DistanceTolerance { get; set; } @@ -63,6 +72,10 @@ namespace Barotrauma.Items.Components set; } + [Editable, Serialize(DirectionType.None, false, description: "Which direction the port is allowed to dock in. For example, \"Top\" would mean the port can dock to another port above it.\n"+ + "Normally there's no need to touch this setting, but if you notice the docking position is incorrect (for example due to some unusual docking port configuration without hulls or doors), you can use this to enforce the direction.")] + public DirectionType ForceDockingDirection { get; set; } + public DockingPort DockingTarget { get; private set; } public Door Door { get; private set; } @@ -89,6 +102,11 @@ namespace Barotrauma.Items.Components } } + public bool IsLocked + { + get { return joint is WeldJoint || DockingTarget?.joint is WeldJoint; } + } + /// /// Automatically cleared after docking -> no need to unregister /// @@ -311,10 +329,10 @@ namespace Barotrauma.Items.Components joint = null; } - Vector2 offset = (IsHorizontal ? + Vector2 offset = IsHorizontal ? Vector2.UnitX * DockingDir : - Vector2.UnitY * DockingDir); - offset *= DockedDistance * 0.5f; + Vector2.UnitY * DockingDir; + offset *= DockedDistance * 0.5f * item.Scale; Vector2 pos1 = item.WorldPosition + offset; @@ -346,6 +364,14 @@ namespace Barotrauma.Items.Components public int GetDir(DockingPort dockingTarget = null) { + int forcedDockingDir = GetForcedDockingDir(); + if (forcedDockingDir != 0) { return forcedDockingDir; } + if (dockingTarget != null) + { + forcedDockingDir = -dockingTarget.GetForcedDockingDir(); + if (forcedDockingDir != 0) { return forcedDockingDir; } + } + if (DockingDir != 0) { return DockingDir; } if (Door != null && Door.LinkedGap.linkedTo.Count > 0) @@ -390,9 +416,10 @@ namespace Barotrauma.Items.Components } if (dockingTarget != null) { - return IsHorizontal ? + int dir = IsHorizontal ? Math.Sign(dockingTarget.item.WorldPosition.X - item.WorldPosition.X) : Math.Sign(dockingTarget.item.WorldPosition.Y - item.WorldPosition.Y); + if (dir != 0) { return dir; } } if (item.Submarine != null) { @@ -404,6 +431,22 @@ namespace Barotrauma.Items.Components return 0; } + private int GetForcedDockingDir() + { + switch (ForceDockingDirection) + { + case DirectionType.Left: + return -1; + case DirectionType.Right: + return 1; + case DirectionType.Top: + return 1; + case DirectionType.Bottom: + return -1; + } + return 0; + } + private void ConnectWireBetweenPorts() { Wire wire = item.GetComponent(); @@ -491,8 +534,9 @@ namespace Barotrauma.Items.Components subs = new Submarine[] { DockingTarget.item.Submarine, item.Submarine }; } - hullRects[0] = new Rectangle(hullRects[0].Center.X, hullRects[0].Y, ((int)DockedDistance / 2), hullRects[0].Height); - hullRects[1] = new Rectangle(hullRects[1].Center.X - ((int)DockedDistance / 2), hullRects[1].Y, ((int)DockedDistance / 2), hullRects[1].Height); + int scaledDockedDistance = (int)(DockedDistance / 2 * item.Scale); + hullRects[0] = new Rectangle(hullRects[0].Center.X, hullRects[0].Y, scaledDockedDistance, hullRects[0].Height); + hullRects[1] = new Rectangle(hullRects[1].Center.X - scaledDockedDistance, hullRects[1].Y, scaledDockedDistance, hullRects[1].Height); //expand hulls if needed, so there's no empty space between the sub's hulls and docking port hulls int leftSubRightSide = int.MinValue, rightSubLeftSide = int.MaxValue; @@ -588,8 +632,9 @@ namespace Barotrauma.Items.Components subs = new Submarine[] { DockingTarget.item.Submarine, item.Submarine }; } - hullRects[0] = new Rectangle(hullRects[0].X, hullRects[0].Y + (int)(-hullRects[0].Height + DockedDistance) / 2, hullRects[0].Width, ((int)DockedDistance / 2)); - hullRects[1] = new Rectangle(hullRects[1].X, hullRects[1].Y - hullRects[1].Height / 2, hullRects[1].Width, ((int)DockedDistance / 2)); + int scaledDockedDistance = (int)(DockedDistance / 2 * item.Scale); + hullRects[0] = new Rectangle(hullRects[0].X, hullRects[0].Y - hullRects[0].Height / 2 + scaledDockedDistance, hullRects[0].Width, scaledDockedDistance); + hullRects[1] = new Rectangle(hullRects[1].X, hullRects[1].Y - hullRects[1].Height / 2, hullRects[1].Width, scaledDockedDistance); //expand hulls if needed, so there's no empty space between the sub's hulls and docking port hulls int upperSubBottom = int.MaxValue, lowerSubTop = int.MinValue; @@ -908,7 +953,6 @@ namespace Barotrauma.Items.Components if (joint is DistanceJoint) { - item.SendSignal(0, "0", "state_out", null); dockingState = MathHelper.Lerp(dockingState, 0.5f, deltaTime * 10.0f); forceLockTimer += deltaTime; @@ -918,9 +962,7 @@ namespace Barotrauma.Items.Components if (jointDiff.LengthSquared() > 0.04f * 0.04f && forceLockTimer < ForceLockDelay) { float totalMass = item.Submarine.PhysicsBody.Mass + DockingTarget.item.Submarine.PhysicsBody.Mass; - float massRatio1 = 1.0f; - float massRatio2 = 1.0f; - + float massRatio1, massRatio2; if (item.Submarine.PhysicsBody.BodyType != BodyType.Dynamic) { massRatio1 = 0.0f; @@ -954,11 +996,10 @@ namespace Barotrauma.Items.Components { doorBody.Enabled = DockingTarget.Door.Body.Enabled; } - - item.SendSignal(0, "1", "state_out", null); - dockingState = MathHelper.Lerp(dockingState, 1.0f, deltaTime * 10.0f); } + + item.SendSignal(0, IsLocked ? "1" : "0", "state_out", null); } if (!obstructedWayPointsDisabled && dockingState >= 0.99f) { @@ -985,7 +1026,8 @@ namespace Barotrauma.Items.Components if (initialized) { return; } initialized = true; - float closestDist = 30.0f * 30.0f; + float maxXDist = (item.Prefab.sprite.size.X * item.Prefab.Scale) / 2; + float closestYDist = (item.Prefab.sprite.size.Y * item.Prefab.Scale) / 2; foreach (Item it in Item.ItemList) { if (it.Submarine != item.Submarine) { continue; } @@ -993,11 +1035,22 @@ namespace Barotrauma.Items.Components var doorComponent = it.GetComponent(); if (doorComponent == null || doorComponent.IsHorizontal == IsHorizontal) { continue; } - float distSqr = Vector2.DistanceSquared(item.Position, it.Position); - if (distSqr < closestDist) + float yDist = Math.Abs(it.Position.Y - item.Position.Y); + if (item.linkedTo.Contains(it)) + { + // If there's a door linked to the docking port, always treat it close enough. + yDist = Math.Min(closestYDist, yDist); + } + else if (Math.Abs(it.Position.X - item.Position.X) > maxXDist) + { + // Too far left/right + continue; + } + + if (yDist <= closestYDist) { Door = doorComponent; - closestDist = distSqr; + closestYDist = yDist; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs index e62daeaa4..c7d133f31 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs @@ -66,6 +66,8 @@ namespace Barotrauma.Items.Components private Rectangle doorRect; private bool isBroken; + + public bool CanBeTraversed => (IsOpen || IsBroken) && !IsJammed && !IsStuck; public bool IsBroken { @@ -283,12 +285,6 @@ namespace Barotrauma.Items.Components return isBroken || base.HasRequiredItems(character, addMessage, msg); } - public bool CanBeOpenedWithoutTools(Character character) - { - if (isBroken) { return true; } - return HasAccess(character); - } - public override bool Pick(Character picker) { if (item.Condition < RepairThreshold) { return true; } @@ -642,6 +638,19 @@ namespace Barotrauma.Items.Components partial void OnFailedToOpen(); + public override bool HasAccess(Character character) + { + if (!item.IsInteractable(character)) { return false; } + if (HasIntegratedButtons) + { + return base.HasAccess(character); + } + else + { + return Item.GetConnectedComponents(true).Any(b => b.HasAccess(character)); + } + } + public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f) { if (IsStuck || IsJammed) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs index 3e98a264c..27c566b83 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs @@ -395,6 +395,9 @@ namespace Barotrauma.Items.Components [Serialize("1,1,1,1", true, "Probability for the plant to grow in a direction.")] public Vector4 GrowthWeights { get; set; } + [Serialize(0.0f, true, "How much damage is taken from fires.")] + public float FireVulnerability { get; set; } + private const float increasedDeathSpeed = 10f; private bool accelerateDeath; private float health; @@ -418,6 +421,7 @@ namespace Barotrauma.Items.Components private int productDelay; private int vineDelay; + private float fireCheckCooldown; public readonly List ProducedItems = new List(); public readonly List Vines = new List(); @@ -489,11 +493,15 @@ namespace Barotrauma.Items.Components if (Health > 0) { GrowVines(planter, slot); - Health -= accelerateDeath ? Hardiness * increasedDeathSpeed : Hardiness; + + // fertilizer makes the plant tick faster, compensate by halving water requirement + float multipler = planter.Fertilizer > 0 ? 0.5f : 1f; + + Health -= (accelerateDeath ? Hardiness * increasedDeathSpeed : Hardiness) * multipler; if (planter.Item.InWater) { - Health -= FloodTolerance; + Health -= FloodTolerance * multipler; } #if SERVER if (FullyGrown) @@ -630,6 +638,8 @@ namespace Barotrauma.Items.Components { base.Update(deltaTime, cam); + UpdateFires(deltaTime); + #if CLIENT foreach (VineTile vine in Vines) { @@ -640,6 +650,29 @@ namespace Barotrauma.Items.Components CheckPlantState(); } + private void UpdateFires(float deltaTime) + { + if (!Decayed && item.CurrentHull?.FireSources is { } fireSources && FireVulnerability > 0f) + { + if (fireCheckCooldown <= 0) + { + foreach (FireSource source in fireSources) + { + if (source.IsInDamageRange(item.WorldPosition, source.DamageRange)) + { + Health -= FireVulnerability; + } + } + + fireCheckCooldown = 5f; + } + else + { + fireCheckCooldown -= deltaTime; + } + } + } + private void GrowVines(Planter planter, PlantSlot slot) { if (FullyGrown) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index b571acff8..ecf87e040 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -18,7 +18,7 @@ namespace Barotrauma.Items.Components protected Vector2[] handlePos; private readonly Vector2[] scaledHandlePos; - private InputType prevPickKey; + private readonly InputType prevPickKey; private string prevMsg; private Dictionary> prevRequiredItems; @@ -29,6 +29,8 @@ namespace Barotrauma.Items.Components private float swingState; + private Character prevEquipper; + private bool attachable, attached, attachedByDefault; private Voronoi2.VoronoiCell attachTargetCell; private readonly PhysicsBody body; @@ -301,11 +303,9 @@ namespace Barotrauma.Items.Components { item.SetTransform(picker.SimPosition, 0.0f); } - } - + } } - picker.DeselectItem(item); picker.Inventory.RemoveItem(item); picker = null; } @@ -340,34 +340,33 @@ namespace Barotrauma.Items.Components } bool alreadyEquipped = character.HasEquippedItem(item); - bool canSelect = picker.TrySelectItem(item); - - if (canSelect || picker.HasEquippedItem(item)) + if (picker.HasEquippedItem(item)) { - if (!canSelect) - { - character.DeselectItem(item); - } - item.body.Enabled = true; item.body.PhysEnabled = false; IsActive = true; #if SERVER - if (!alreadyEquipped) GameServer.Log(GameServer.CharacterLogName(character) + " equipped " + item.Name, ServerLog.MessageType.ItemInteraction); + if (picker != prevEquipper) { GameServer.Log(GameServer.CharacterLogName(character) + " equipped " + item.Name, ServerLog.MessageType.ItemInteraction); } #endif + prevEquipper = picker; + } + else + { + prevEquipper = null; } } public override void Unequip(Character character) { - if (picker == null) { return; } - - picker.DeselectItem(item); #if SERVER - GameServer.Log(GameServer.CharacterLogName(character) + " unequipped " + item.Name, ServerLog.MessageType.ItemInteraction); + if (prevEquipper != null) + { + GameServer.Log(GameServer.CharacterLogName(character) + " unequipped " + item.Name, ServerLog.MessageType.ItemInteraction); + } #endif - + prevEquipper = null; + if (picker == null) { return; } item.body.PhysEnabled = true; item.body.Enabled = false; IsActive = false; @@ -488,13 +487,12 @@ namespace Barotrauma.Items.Components } } - var containedItems = item.OwnInventory?.Items; + var containedItems = item.OwnInventory?.AllItems; if (containedItems != null) { foreach (Item contained in containedItems) { - if (contained == null) { continue; } - if (contained.body == null) { continue; } + if (contained?.body == null) { continue; } contained.SetTransform(item.SimPosition, contained.body.Rotation); } } @@ -673,7 +671,7 @@ namespace Barotrauma.Items.Components item.Submarine = picker.Submarine; - if (picker.HasSelectedItem(item)) + if (picker.HeldItems.Contains(item)) { scaledHandlePos[0] = handlePos[0] * item.Scale; scaledHandlePos[1] = handlePos[1] * item.Scale; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs index fd67498e6..d426b1a70 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs @@ -42,13 +42,13 @@ namespace Barotrauma.Items.Components public override void Equip(Character character) { base.Equip(character); - character.Info.CheckDisguiseStatus(true, this); + character.Info?.CheckDisguiseStatus(true, this); } public override void Unequip(Character character) { base.Unequip(character); - character.Info.CheckDisguiseStatus(true, this); + character.Info?.CheckDisguiseStatus(true, this); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs index adf3e7c01..d3849f9bf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs @@ -58,6 +58,13 @@ namespace Barotrauma.Items.Components } } + [Serialize(1.0f, false, description: "How much the position of the item can vary from the wall the item spawns on.")] + public float RandomOffsetFromWall + { + get; + set; + } + public bool Attached { get { return holdable != null && holdable.Attached; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index dad1c77cf..abae924a3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -50,6 +50,11 @@ namespace Barotrauma.Items.Components set; } + /// + /// Defines items that boost the weapon functionality, like battery cell for stun batons. + /// + public readonly string[] PreferredContainedItems; + public MeleeWeapon(Item item, XElement element) : base(item, element) { @@ -61,6 +66,7 @@ namespace Barotrauma.Items.Components item.IsShootable = true; // TODO: should define this in xml if we have melee weapons that don't require aim to use item.RequireAimToUse = true; + PreferredContainedItems = element.GetAttributeStringArray("preferredcontaineditems", new string[0], convertToLowerInvariant: true); } public override void Equip(Character character) @@ -76,11 +82,9 @@ namespace Barotrauma.Items.Components if (Item.RequireAimToUse && !character.IsKeyDown(InputType.Aim) || hitting) { return false; } //don't allow hitting if the character is already hitting with another weapon - for (int i = 0; i < 2; i++ ) + foreach (Item heldItem in character.HeldItems) { - if (character.SelectedItems[i] == null || character.SelectedItems[i] == Item) { continue; } - - var otherWeapon = character.SelectedItems[i].GetComponent(); + var otherWeapon = heldItem.GetComponent(); if (otherWeapon == null) { continue; } if (otherWeapon.hitting) { return false; } } @@ -143,7 +147,7 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { if (!item.body.Enabled) { impactQueue.Clear(); return; } - if (picker == null && !picker.HasSelectedItem(item)) { impactQueue.Clear(); IsActive = false; } + if (picker == null && !picker.HeldItems.Contains(item)) { impactQueue.Clear(); IsActive = false; } while (impactQueue.Count > 0) { @@ -244,12 +248,14 @@ namespace Barotrauma.Items.Components return true; } - //ignore collision if there's a wall between the user and the weapon to prevent hitting through walls + contact.GetWorldManifold(out Vector2 normal, out var points); + + //ignore collision if there's a wall between the user and the contact point to prevent hitting through walls if (Submarine.PickBody(User.AnimController.AimSourceSimPos, - item.SimPosition, + points[0], collisionCategory: Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking, allowInsideFixture: true, - customPredicate: (Fixture fixture) => { return fixture.CollidesWith.HasFlag(Physics.CollisionItem); }) != null) + customPredicate: (Fixture fixture) => { return fixture.CollidesWith.HasFlag(Physics.CollisionItem) && fixture.Body != f2.Body; }) != null) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs index 31e733992..e0a31335b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs @@ -2,6 +2,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Linq; using System.Xml.Linq; namespace Barotrauma.Items.Components @@ -91,7 +92,7 @@ namespace Barotrauma.Items.Components { if (picker.Inventory.TryPutItemWithAutoEquipCheck(item, picker, allowedSlots)) { - if (!picker.HasSelectedItem(item) && item.body != null) item.body.Enabled = false; + if (!picker.HeldItems.Contains(item) && item.body != null) { item.body.Enabled = false; } this.picker = picker; for (int i = item.linkedTo.Count - 1; i >= 0; i--) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs index 606dc01c2..945b05189 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs @@ -71,11 +71,11 @@ namespace Barotrauma.Items.Components character.AnimController.Collider.ApplyForce(propulsion, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); - if (character.SelectedItems[0] == item) + if (character.Inventory.IsInLimbSlot(item, InvSlotType.RightHand)) { character.AnimController.GetLimb(LimbType.RightHand)?.body.ApplyForce(propulsion, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); } - if (character.SelectedItems[1] == item) + if (character.Inventory.IsInLimbSlot(item, InvSlotType.LeftHand)) { character.AnimController.GetLimb(LimbType.LeftHand)?.body.ApplyForce(propulsion, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs index 967b4ecb7..0a8abef5a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs @@ -152,7 +152,7 @@ namespace Barotrauma.Items.Components public Projectile FindProjectile(bool triggerOnUseOnContainers = false) { - var containedItems = item.OwnInventory?.Items; + var containedItems = item.OwnInventory?.AllItemsMod; if (containedItems == null) { return null; } foreach (Item item in containedItems) @@ -166,7 +166,7 @@ namespace Barotrauma.Items.Components foreach (Item it in containedItems) { if (it == null) { continue; } - var containedSubItems = it.OwnInventory?.Items; + var containedSubItems = it.OwnInventory?.AllItemsMod; if (containedSubItems == null) { continue; } foreach (Item subItem in containedSubItems) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index 69eee9534..4fa45a4b8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -87,6 +87,13 @@ namespace Barotrauma.Items.Components [Serialize(false, false, description: "Can the item repair things through holes in walls.")] public bool RepairThroughHoles { get; set; } + + [Serialize(100.0f, false, description: "How far two walls need to not be considered overlapping and to stop the ray.")] + public float MaxOverlappingWallDist + { + get; set; + } + [Serialize(true, false, description: "Can the item hit broken doors.")] public bool HitItems { get; set; } @@ -320,9 +327,14 @@ namespace Barotrauma.Items.Components { var bodies = Submarine.PickBodies(rayStart, rayEnd, ignoredBodies, collisionCategories, ignoreSensors: false, - customPredicate: (Fixture f) => + customPredicate: (Fixture f) => { - if (RepairThroughHoles && f.IsSensor && f.Body?.UserData is Structure || (f.Body?.UserData is Item it && it.GetComponent() != null)) { return false; } + if (f.IsSensor) + { + if (RepairThroughHoles && f.Body?.UserData is Structure) { return false; } + if (f.Body?.UserData is PhysicsBody) { return false; } + } + if (f.Body?.UserData is Item it && it.GetComponent() != null) { return false; } if (f.Body?.UserData as string == "ruinroom") { return false; } if (f.Body?.UserData is VineTile && !(FireDamage > 0)) { return false; } return true; @@ -361,12 +373,13 @@ namespace Barotrauma.Items.Components } //if repairing through walls is not allowed and the next wall is more than 100 pixels away from the previous one, stop here - //(= repairing multiple overlapping walls is allowed as long as the edges of the walls are less than 100 pixels apart) + //(= repairing multiple overlapping walls is allowed as long as the edges of the walls are less than MaxOverlappingWallDist pixels apart) float thisBodyFraction = Submarine.LastPickedBodyDist(body); - if (!RepairThroughWalls && lastHitType == typeof(Structure) && Range * (thisBodyFraction - lastPickedFraction) > 100.0f) + if (!RepairThroughWalls && lastHitType == typeof(Structure) && Range * (thisBodyFraction - lastPickedFraction) > MaxOverlappingWallDist) { break; } + pickedPosition = rayStart + (rayEnd - rayStart) * thisBodyFraction; if (FixBody(user, deltaTime, degreeOfSuccess, body)) { lastPickedFraction = thisBodyFraction; @@ -376,13 +389,16 @@ namespace Barotrauma.Items.Components } else { - FixBody(user, deltaTime, degreeOfSuccess, - Submarine.PickBody(rayStart, rayEnd, - ignoredBodies, collisionCategories, + var pickedBody = Submarine.PickBody(rayStart, rayEnd, + ignoredBodies, collisionCategories, ignoreSensors: false, - customPredicate: (Fixture f) => + customPredicate: (Fixture f) => { - if (RepairThroughHoles && f.IsSensor && f.Body?.UserData is Structure) { return false; } + if (f.IsSensor) + { + if (RepairThroughHoles && f.Body?.UserData is Structure) { return false; } + if (f.Body?.UserData is PhysicsBody) { return false; } + } if (f.Body?.UserData as string == "ruinroom") { return false; } if (f.Body?.UserData is VineTile && !(FireDamage > 0)) { return false; } @@ -398,9 +414,11 @@ namespace Barotrauma.Items.Components if (targetItem.Condition <= 0) { return false; } } } - return f.Body?.UserData != null; + return f.Body?.UserData != null; }, - allowInsideFixture: true)); + allowInsideFixture: true); + pickedPosition = Submarine.LastPickedPosition; + FixBody(user, deltaTime, degreeOfSuccess, pickedBody); lastPickedFraction = Submarine.LastPickedFraction; } @@ -495,8 +513,6 @@ namespace Barotrauma.Items.Components { if (targetBody?.UserData == null) { return false; } - pickedPosition = Submarine.LastPickedPosition; - if (targetBody.UserData is Structure targetStructure) { if (targetStructure.IsPlatform) { return false; } @@ -526,8 +542,7 @@ namespace Barotrauma.Items.Components } else if (targetBody.UserData is Voronoi2.VoronoiCell cell && cell.IsDestructible) { - var levelWall = Level.Loaded?.ExtraWalls.Find(w => w.Body == cell.Body) as DestructibleLevelWall; - if (levelWall != null) + if (Level.Loaded?.ExtraWalls.Find(w => w.Body == cell.Body) is DestructibleLevelWall levelWall) { levelWall.AddDamage(-LevelWallFixAmount * deltaTime, item.WorldPosition); } @@ -579,7 +594,7 @@ namespace Barotrauma.Items.Components } else if (targetBody.UserData is Item targetItem) { - if (!HitItems || targetItem.NonInteractable) { return false; } + if (!HitItems || !targetItem.IsInteractable(user)) { return false; } var levelResource = targetItem.GetComponent(); if (levelResource != null && levelResource.Attached && @@ -594,7 +609,7 @@ namespace Barotrauma.Items.Components levelResource.DeattachTimer / levelResource.DeattachDuration, GUI.Style.Red, GUI.Style.Green, "progressbar.deattaching"); #endif - FixItemProjSpecific(user, deltaTime, targetItem); + FixItemProjSpecific(user, deltaTime, targetItem, showProgressBar: false); return true; } @@ -620,7 +635,7 @@ namespace Barotrauma.Items.Components targetItem.body.ApplyForce(dir * TargetForce, maxVelocity: 10.0f); } - FixItemProjSpecific(user, deltaTime, targetItem); + FixItemProjSpecific(user, deltaTime, targetItem, showProgressBar: true); return true; } else if (targetBody.UserData is BallastFloraBranch branch) @@ -635,7 +650,7 @@ namespace Barotrauma.Items.Components partial void FixStructureProjSpecific(Character user, float deltaTime, Structure targetStructure, int sectionIndex); partial void FixCharacterProjSpecific(Character user, float deltaTime, Character targetCharacter); - partial void FixItemProjSpecific(Character user, float deltaTime, Item targetItem); + partial void FixItemProjSpecific(Character user, float deltaTime, Item targetItem, bool showProgressBar); private float sinTime; private float repairTimer; @@ -643,74 +658,71 @@ namespace Barotrauma.Items.Components private readonly float repairTimeOut = 5; public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { - if (!(objective.OperateTarget is Gap leak)) { return true; } - if (leak.Submarine == null) { return true; } + if (!(objective.OperateTarget is Gap leak)) + { + Reset(); + return true; + } + if (leak.Submarine == null) + { + Reset(); + return true; + } if (leak != previousGap) { - sinTime = 0; - repairTimer = 0; + Reset(); previousGap = leak; } Vector2 fromCharacterToLeak = leak.WorldPosition - character.WorldPosition; float dist = fromCharacterToLeak.Length(); float reach = AIObjectiveFixLeak.CalculateReach(this, character); - //too far away -> consider this done and hope the AI is smart enough to move closer - if (dist > reach * 2) { return true; } - character.AIController.SteeringManager.Reset(); - //steer closer if almost in range - if (dist > reach) + if (dist > reach * 3) { - if (character.AnimController.InWater) + // Too far away -> consider this done and hope the AI is smart enough to move closer + Reset(); + return true; + } + character.AIController.SteeringManager.Reset(); + if (!character.AnimController.InWater) + { + // TODO: use the collider size? + if (!character.AnimController.InWater && character.AnimController is HumanoidAnimController && + Math.Abs(fromCharacterToLeak.X) < 100.0f && fromCharacterToLeak.Y < 0.0f && fromCharacterToLeak.Y > -150.0f) { - if (character.AIController.SteeringManager is IndoorsSteeringManager indoorSteering) + ((HumanoidAnimController)character.AnimController).Crouching = true; + } + } + if (dist > reach * 0.8f || dist > reach * 0.5f && character.AnimController.Limbs.Any(l => l.inWater)) + { + // Steer closer + if (character.AIController.SteeringManager is IndoorsSteeringManager indoorSteering) + { + // Swimming inside the sub + if (indoorSteering.CurrentPath != null && !indoorSteering.IsPathDirty && (indoorSteering.CurrentPath.Unreachable || indoorSteering.CurrentPath.Finished)) { - // Swimming inside the sub - if (indoorSteering.CurrentPath != null && !indoorSteering.IsPathDirty && indoorSteering.CurrentPath.Unreachable) - { - Vector2 dir = Vector2.Normalize(fromCharacterToLeak); - character.AIController.SteeringManager.SteeringManual(deltaTime, dir); - } - else - { - character.AIController.SteeringManager.SteeringSeek(character.GetRelativeSimPosition(leak)); - } + Vector2 dir = Vector2.Normalize(fromCharacterToLeak); + character.AIController.SteeringManager.SteeringManual(deltaTime, dir); } else { - // Swimming outside the sub character.AIController.SteeringManager.SteeringSeek(character.GetRelativeSimPosition(leak)); } } else { - // TODO: use the collider size? - if (!character.AnimController.InWater && character.AnimController is HumanoidAnimController && - Math.Abs(fromCharacterToLeak.X) < 100.0f && fromCharacterToLeak.Y < 0.0f && fromCharacterToLeak.Y > -150.0f) - { - ((HumanoidAnimController)character.AnimController).Crouching = true; - } - Vector2 standPos = new Vector2(Math.Sign(-fromCharacterToLeak.X), Math.Sign(-fromCharacterToLeak.Y)) / 2; - if (leak.IsHorizontal) - { - standPos.X *= 2; - standPos.Y = 0; - } - else - { - standPos.X = 0; - } - character.AIController.SteeringManager.SteeringSeek(standPos); + // Swimming outside the sub + character.AIController.SteeringManager.SteeringSeek(character.GetRelativeSimPosition(leak)); } } - if (dist < reach / 2) + else if (dist < reach * 0.25f) { // Too close -> steer away character.AIController.SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.SimPosition - leak.SimPosition)); } - else if (dist < reach * 2) + if (dist <= reach) { - // In or almost in range + // In range character.CursorPosition = leak.WorldPosition; if (character.Submarine != null) { @@ -734,46 +746,52 @@ namespace Barotrauma.Items.Components character.AIController.SteeringManager.SteeringManual(deltaTime, moveDir); } } - } - if (item.RequireAimToUse) - { - character.SetInput(InputType.Aim, false, true); - sinTime += deltaTime * 5; - } - // Press the trigger only when the tool is approximately facing the target. - Vector2 fromItemToLeak = leak.WorldPosition - item.WorldPosition; - var angle = VectorExtensions.Angle(VectorExtensions.Forward(item.body.TransformedRotation), fromItemToLeak); - if (angle < MathHelper.PiOver4) - { - if (Submarine.PickBody(item.SimPosition, leak.SimPosition, collisionCategory: Physics.CollisionWall, allowInsideFixture: true)?.UserData is Item i) + if (item.RequireAimToUse) { - var door = i.GetComponent(); - // Hit a door, abandon so that we don't weld it shut. - return door != null && !door.IsOpen && !door.IsBroken; + character.SetInput(InputType.Aim, false, true); + sinTime += deltaTime * 5; } - // Check that we don't hit any friendlies - if (Submarine.PickBodies(item.SimPosition, leak.SimPosition, collisionCategory: Physics.CollisionCharacter).None(hit => + // Press the trigger only when the tool is approximately facing the target. + Vector2 fromItemToLeak = leak.WorldPosition - item.WorldPosition; + var angle = VectorExtensions.Angle(VectorExtensions.Forward(item.body.TransformedRotation), fromItemToLeak); + if (angle < MathHelper.PiOver4) { - if (hit.UserData is Character c) + if (Submarine.PickBody(item.SimPosition, leak.SimPosition, collisionCategory: Physics.CollisionWall, allowInsideFixture: true)?.UserData is Item i) { - if (c == character) { return false; } - return HumanAIController.IsFriendly(character, c); + var door = i.GetComponent(); + // Hit a door, abandon so that we don't weld it shut. + return door != null && !door.CanBeTraversed; } - return false; - })) - { - character.SetInput(InputType.Shoot, false, true); - Use(deltaTime, character); - repairTimer += deltaTime; - if (repairTimer > repairTimeOut) + // Check that we don't hit any friendlies + if (Submarine.PickBodies(item.SimPosition, leak.SimPosition, collisionCategory: Physics.CollisionCharacter).None(hit => { + if (hit.UserData is Character c) + { + if (c == character) { return false; } + return HumanAIController.IsFriendly(character, c); + } + return false; + })) + { + character.SetInput(InputType.Shoot, false, true); + Use(deltaTime, character); + repairTimer += deltaTime; + if (repairTimer > repairTimeOut) + { #if DEBUG - DebugConsole.NewMessage($"{character.Name}: timed out while welding a leak in {leak.FlowTargetHull.DisplayName}.", color: Color.Yellow); + DebugConsole.NewMessage($"{character.Name}: timed out while welding a leak in {leak.FlowTargetHull.DisplayName}.", color: Color.Yellow); #endif - return true; + Reset(); + return true; + } } } } + else + { + // Reset the timer so that we don't time out if the water forces push us away + repairTimer = 0; + } bool leakFixed = (leak.Open <= 0.0f || leak.Removed) && (leak.ConnectedWall == null || leak.ConnectedWall.Sections.Average(s => s.damage) < 1); @@ -791,6 +809,12 @@ namespace Barotrauma.Items.Components } return leakFixed; + + void Reset() + { + sinTime = 0; + repairTimer = 0; + } } private void ApplyStatusEffectsOnTarget(Character user, float deltaTime, ActionType actionType, IEnumerable targets) @@ -821,7 +845,7 @@ namespace Barotrauma.Items.Components foreach (ISerializableEntity target in targets) { if (!(target is Door door)) { continue; } - if (!door.CanBeWelded || door.Item.NonInteractable) { continue; } + if (!door.CanBeWelded || !door.Item.IsInteractable(user)) { continue; } for (int i = 0; i < effect.propertyNames.Length; i++) { string propertyName = effect.propertyNames[i]; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs index ac2f0ac8b..143226000 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs @@ -1,5 +1,6 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; +using System.Linq; using System.Xml.Linq; namespace Barotrauma.Items.Components @@ -83,7 +84,7 @@ namespace Barotrauma.Items.Components return; } - if (picker == null || picker.Removed || !picker.HasSelectedItem(item)) + if (picker == null || picker.Removed || !picker.HeldItems.Contains(item)) { IsActive = false; return; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index deb68328f..dd00b291c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -235,6 +235,12 @@ namespace Barotrauma.Items.Components [Serialize(0f, false, description: "How useful the item is in combat? Used by AI to decide which item it should use as a weapon. For the sake of clarity, use a value between 0 and 100 (not enforced).")] public float CombatPriority { get; private set; } + /// + /// Which sound should be played when manual sound selection type is selected? Not [Editable] because we don't want this visible in the editor for every component. + /// + [Serialize(0, true, alwaysUseInstanceValues: true)] + public int ManuallySelectedSound { get; private set; } + public ItemComponent(Item item, XElement element) { this.item = item; @@ -453,22 +459,20 @@ namespace Barotrauma.Items.Components public virtual bool Combine(Item item, Character user) { - if (canBeCombined && this.item.Prefab == item.Prefab && item.Condition > 0.0f && this.item.Condition > 0.0f) + if (canBeCombined && this.item.Prefab == item.Prefab && + item.Condition > 0.0f && this.item.Condition > 0.0f && + !item.IsFullCondition && !this.item.IsFullCondition) { - float transferAmount = 0.0f; - if (this.Item.Condition <= item.Condition) - transferAmount = Math.Min(item.Condition, this.item.MaxCondition - this.item.Condition); - else - transferAmount = -Math.Min(this.item.Condition, item.MaxCondition - item.Condition); + float transferAmount = Math.Min(item.Condition, this.item.MaxCondition - this.item.Condition); - if (transferAmount == 0.0f) { return false; } + if (MathUtils.NearlyEqual(transferAmount, 0.0f)) { return false; } if (removeOnCombined) { if (item.Condition - transferAmount <= 0.0f) { if (item.ParentInventory != null) { - if (item.ParentInventory.Owner is Character owner && owner.HasSelectedItem(item)) + if (item.ParentInventory.Owner is Character owner && owner.HeldItems.Contains(item)) { item.Unequip(owner); } @@ -484,7 +488,7 @@ namespace Barotrauma.Items.Components { if (this.Item.ParentInventory != null) { - if (this.Item.ParentInventory.Owner is Character owner && owner.HasSelectedItem(this.Item)) + if (this.Item.ParentInventory.Owner is Character owner && owner.HeldItems.Contains(this.Item)) { this.Item.Unequip(owner); } @@ -654,16 +658,18 @@ namespace Barotrauma.Items.Components /// /// Only checks if any of the Picked requirements are matched (used for checking id card(s)). Much simpler and a bit different than HasRequiredItems. /// - public bool HasAccess(Character character) + public virtual bool HasAccess(Character character) { - if (character.Inventory == null) { return false; } + if (!item.IsInteractable(character)) { return false; } if (requiredItems.None()) { return true; } - - foreach (Item item in character.Inventory.Items) + if (character.Inventory != null) { - if (requiredItems.Any(ri => ri.Value.Any(r => r.Type == RelatedItem.RelationType.Picked && r.MatchesItem(item)))) + foreach (Item item in character.Inventory.AllItems) { - return true; + if (requiredItems.Any(ri => ri.Value.Any(r => r.Type == RelatedItem.RelationType.Picked && r.MatchesItem(item)))) + { + return true; + } } } return false; @@ -672,6 +678,7 @@ namespace Barotrauma.Items.Components public virtual bool HasRequiredItems(Character character, bool addMessage, string msg = null) { if (requiredItems.None()) { return true; } + if (!character.IsPlayer && character.Params.AI != null && character.Params.AI.Infiltrate) { return true; } if (character.Inventory == null) { return false; } bool hasRequiredItems = false; bool canContinue = true; @@ -679,7 +686,7 @@ namespace Barotrauma.Items.Components { foreach (RelatedItem ri in requiredItems[RelatedItem.RelationType.Equipped]) { - canContinue = CheckItems(ri, character.SelectedItems); + canContinue = CheckItems(ri, character.HeldItems); if (!canContinue) { break; } } } @@ -689,7 +696,7 @@ namespace Barotrauma.Items.Components { foreach (RelatedItem ri in requiredItems[RelatedItem.RelationType.Picked]) { - if (!CheckItems(ri, character.Inventory.Items)) { break; } + if (!CheckItems(ri, character.Inventory.AllItems)) { break; } } } } @@ -957,7 +964,7 @@ namespace Barotrauma.Items.Components previousUser = character; itemIndex = 0; } - if (character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: aiController.IgnoredItems, customPriorityFunction: priority)) + if (character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: aiController.IgnoredItems, customPriorityFunction: priority, positionalReference: Item)) { suitableContainer = targetContainer; return true; @@ -1016,7 +1023,7 @@ namespace Barotrauma.Items.Components if (character.AIController is HumanAIController aiController) { ItemContainer sourceC = sourceContainer ?? (item.OwnInventory?.Owner is Item it ? it.GetComponent() : null); - var containedItems = sourceContainer != null ? sourceContainer.Inventory.Items : item.OwnInventory.Items; + var containedItems = sourceContainer != null ? sourceContainer.Inventory.AllItems : item.OwnInventory.AllItems; foreach (Item containedItem in containedItems) { if (containedItem != null && containedItem.Condition <= 0.0f) @@ -1027,20 +1034,20 @@ namespace Barotrauma.Items.Components if (i.IsThisOrAnyContainerIgnoredByAI()) { return 0; } var container = i.GetComponent(); if (container == null) { return 0; } - if (container.Inventory.IsFull()) { return 0; } + if (!container.Inventory.CanBePut(containedItem)) { return 0; } // Ignore containers that are identical to the source container if (sourceC != null && container.Item.Prefab == sourceC.Item.Prefab) { return 0; } if (container.ShouldBeContained(containedItem, out bool isRestrictionsDefined)) { if (isRestrictionsDefined) { - return 4; + return 10; } else { - if (containedItem.Prefab.IsContainerPreferred(container, out bool isPreferencesDefined, out bool isSecondary)) + if (containedItem.IsContainerPreferred(container, out bool isPreferencesDefined, out bool isSecondary)) { - return isPreferencesDefined ? isSecondary ? 2 : 3 : 1; + return isPreferencesDefined ? isSecondary ? 2 : 5 : 1; } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index 133216387..3c1ecfaa7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -9,11 +9,24 @@ namespace Barotrauma.Items.Components { partial class ItemContainer : ItemComponent, IDrawableComponent { + class ActiveContainedItem + { + public readonly Item Item; + public readonly StatusEffect StatusEffect; + public readonly bool ExcludeBroken; + public ActiveContainedItem(Item item, StatusEffect statusEffect, bool excludeBroken) + { + Item = item; + StatusEffect = statusEffect; + ExcludeBroken = excludeBroken; + } + } + public ItemInventory Inventory; - private List> itemsWithStatusEffects; + private readonly List activeContainedItems = new List(); - private ushort[] itemIds; + private List[] itemIds; //how many items can be contained private int capacity; @@ -24,6 +37,15 @@ namespace Barotrauma.Items.Components set { capacity = Math.Max(value, 1); } } + //how many items can be contained + private int maxStackSize; + [Serialize(64, false, description: "How many items can be stacked in one slot. Does not increase the maximum stack size of the items themselves, e.g. a stack of bullets could have a maximum size of 8 but the number of bullets in a specific weapon could be restricted to 6.")] + public int MaxStackSize + { + get { return maxStackSize; } + set { maxStackSize = Math.Max(value, 1); } + } + private bool hideItems; [Serialize(true, false, description: "Should the items contained inside this item be hidden." + " If set to false, you should use the ItemPos and ItemInterval properties to determine where the items get rendered.")] @@ -141,8 +163,6 @@ namespace Barotrauma.Items.Components } InitProjSpecific(element); - - itemsWithStatusEffects = new List>(); } partial void InitProjSpecific(XElement element); @@ -154,23 +174,23 @@ namespace Barotrauma.Items.Components RelatedItem ri = ContainableItems.Find(x => x.MatchesItem(containedItem)); if (ri != null) { - itemsWithStatusEffects.RemoveAll(i => i.First == containedItem); + activeContainedItems.RemoveAll(i => i.Item == containedItem); foreach (StatusEffect effect in ri.statusEffects) { - itemsWithStatusEffects.Add(new Pair(containedItem, effect)); + activeContainedItems.Add(new ActiveContainedItem(containedItem, effect, ri.ExcludeBroken)); } } //no need to Update() if this item has no statuseffects and no physics body - IsActive = itemsWithStatusEffects.Count > 0 || Inventory.Items.Any(it => it?.body != null); + IsActive = activeContainedItems.Count > 0 || Inventory.AllItems.Any(it => it.body != null); } public void OnItemRemoved(Item containedItem) { - itemsWithStatusEffects.RemoveAll(i => i.First == containedItem); + activeContainedItems.RemoveAll(i => i.Item == containedItem); //deactivate if the inventory is empty - IsActive = itemsWithStatusEffects.Count > 0 || Inventory.Items.Any(it => it?.body != null); + IsActive = activeContainedItems.Count > 0 || Inventory.AllItems.Any(it => it.body != null); } public bool CanBeContained(Item item) @@ -196,18 +216,17 @@ namespace Barotrauma.Items.Components { item.SetContainedItemPositions(); } - else if (itemsWithStatusEffects.Count == 0) + else if (activeContainedItems.Count == 0) { IsActive = false; return; } - foreach (Pair itemAndEffect in itemsWithStatusEffects) + foreach (var activeContainedItem in activeContainedItems) { - Item contained = itemAndEffect.First; - if (contained.Condition <= 0.0f) continue; - - StatusEffect effect = itemAndEffect.Second; + Item contained = activeContainedItem.Item; + if (activeContainedItem.ExcludeBroken && contained.Condition <= 0.0f) { continue; } + StatusEffect effect = activeContainedItem.StatusEffect; if (effect.HasTargetType(StatusEffect.TargetType.This)) effect.Apply(ActionType.OnContaining, deltaTime, item, item.AllPropertyObjects); @@ -240,9 +259,8 @@ namespace Barotrauma.Items.Components } if (AutoInteractWithContained && character.SelectedConstruction == null) { - foreach (Item contained in Inventory.Items) + foreach (Item contained in Inventory.AllItems) { - if (contained == null) continue; if (contained.TryInteract(character)) { character.FocusedItem = contained; @@ -264,9 +282,8 @@ namespace Barotrauma.Items.Components } if (AutoInteractWithContained) { - foreach (Item contained in Inventory.Items) + foreach (Item contained in Inventory.AllItems) { - if (contained == null) continue; if (contained.TryInteract(picker)) { picker.FocusedItem = contained; @@ -277,20 +294,19 @@ namespace Barotrauma.Items.Components IsActive = true; - return (picker != null); + return picker != null; } public override bool Combine(Item item, Character user) { if (!AllowDragAndDrop && user != null) { return false; } - - if (!ContainableItems.Any(x => x.MatchesItem(item))) { return false; } + if (!ContainableItems.Any(it => it.MatchesItem(item))) { return false; } if (user != null && !user.CanAccessInventory(Inventory)) { return false; } - if (Inventory.TryPutItem(item, null)) + if (Inventory.TryPutItem(item, user)) { IsActive = true; - if (hideItems && item.body != null) item.body.Enabled = false; + if (hideItems && item.body != null) { item.body.Enabled = false; } return true; } @@ -318,9 +334,8 @@ namespace Barotrauma.Items.Components currentRotation += item.body.Rotation; } - foreach (Item contained in Inventory.Items) + foreach (Item contained in Inventory.AllItems) { - if (contained == null) { continue; } if (contained.body != null) { try @@ -362,13 +377,22 @@ namespace Barotrauma.Items.Components public override void OnMapLoaded() { - if (itemIds != null) - { + if (itemIds != null) + { for (ushort i = 0; i < itemIds.Length; i++) { - if (!(Entity.FindEntityByID(itemIds[i]) is Item item)) { continue; } - if (i >= Inventory.Capacity) { continue; } - Inventory.TryPutItem(item, i, false, false, null, false); + if (i >= Inventory.Capacity) + { + //legacy support: before item stacking was implemented, revolver for example had a separate slot for each bullet + //now there's just one, try to put the extra items where they fit (= stack them) + Inventory.TryPutItem(item, user: null, createNetworkEvent: false); + continue; + } + foreach (ushort id in itemIds[i]) + { + if (!(Entity.FindEntityByID(id) is Item item)) { continue; } + Inventory.TryPutItem(item, i, false, false, null, false); + } } itemIds = null; } @@ -380,7 +404,7 @@ namespace Barotrauma.Items.Components if (SpawnWithId.Length > 0) { ItemPrefab prefab = ItemPrefab.Prefabs.Find(m => m.Identifier == SpawnWithId); - if (prefab != null && Inventory != null && Inventory.Items.Any(it => it == null)) + if (prefab != null && Inventory != null && Inventory.CanBePut(prefab)) { Entity.Spawner?.AddToSpawnQueue(prefab, Inventory, spawnIfInventoryFull: false); } @@ -407,13 +431,8 @@ namespace Barotrauma.Items.Components return; } #endif - - foreach (Item item in Inventory.Items) - { - if (item == null) continue; - item.Drop(null); - } - } + Inventory.AllItemsMod.ForEach(it => it.Drop(null)); + } public override void Load(XElement componentElement, bool usePrefabValues, IdRemap idRemap) { @@ -421,26 +440,28 @@ namespace Barotrauma.Items.Components string containedString = componentElement.GetAttributeString("contained", ""); string[] itemIdStrings = containedString.Split(','); - itemIds = new ushort[itemIdStrings.Length]; + itemIds = new List[itemIdStrings.Length]; for (int i = 0; i < itemIdStrings.Length; i++) { - if (!int.TryParse(itemIdStrings[i], out int id)) { continue; } - itemIds[i] = idRemap.GetOffsetId(id); + itemIds[i] ??= new List(); + foreach (string idStr in itemIdStrings[i].Split(';')) + { + if (!int.TryParse(idStr, out int id)) { continue; } + itemIds[i].Add(idRemap.GetOffsetId(id)); + } } } public override XElement Save(XElement parentElement) { XElement componentElement = base.Save(parentElement); - - string[] itemIdStrings = new string[Inventory.Items.Length]; - for (int i = 0; i < Inventory.Items.Length; i++) + string[] itemIdStrings = new string[Inventory.Capacity]; + for (int i = 0; i < Inventory.Capacity; i++) { - itemIdStrings[i] = (Inventory.Items[i] == null) ? "0" : Inventory.Items[i].ID.ToString(); + var items = Inventory.GetItemsAt(i); + itemIdStrings[i] = string.Join(';', items.Select(it => it.ID.ToString())); } - - componentElement.Add(new XAttribute("contained", string.Join(",", itemIdStrings))); - + componentElement.Add(new XAttribute("contained", string.Join(',', itemIdStrings))); return componentElement; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs index d32d542bb..710414f2e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs @@ -90,6 +90,13 @@ namespace Barotrauma.Items.Components [Serialize(UseEnvironment.Both, false, description: "Can the item be selected in air, underwater or both.")] public UseEnvironment UsableIn { get; set; } + [Serialize(false, false, description: "Should the character using the item be drawn behind the item.")] + public bool DrawUserBehind + { + get; + set; + } + public bool ControlCharacterPose { get { return limbPositions.Count > 0; } @@ -236,12 +243,12 @@ namespace Barotrauma.Items.Components case LimbType.RightHand: case LimbType.RightForearm: case LimbType.RightArm: - if (user.SelectedItems[0] != null) { continue; } + if (user.Inventory.GetItemInLimbSlot(InvSlotType.RightHand) != null) { continue; } break; case LimbType.LeftHand: case LimbType.LeftForearm: case LimbType.LeftArm: - if ( user.SelectedItems[1] != null) { continue; } + if (user.Inventory.GetItemInLimbSlot(InvSlotType.LeftHand) != null) { continue; } break; } } @@ -388,6 +395,12 @@ namespace Barotrauma.Items.Components limb.PullJointEnabled = false; } + //disable flipping for 0.5 seconds, because flipping the character when it's in a weird pose (e.g. lying in bed) can mess up the ragdoll + if (character.AnimController is HumanoidAnimController humanoidAnim) + { + humanoidAnim.LockFlippingUntil = (float)Timing.TotalTime + 0.5f; + } + if (character.SelectedConstruction == this.item) { character.SelectedConstruction = null; } character.AnimController.Anim = AnimController.Animation.None; @@ -470,6 +483,12 @@ namespace Barotrauma.Items.Components } } + public override bool HasAccess(Character character) + { + if (!item.IsInteractable(character)) { return false; } + return base.HasAccess(character); + } + partial void HideHUDs(bool value); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs index 660f1417a..2894aa450 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs @@ -1,5 +1,7 @@ -using Barotrauma.Networking; +using Barotrauma.Extensions; +using Barotrauma.Networking; using System; +using System.Collections.Generic; using System.Linq; using System.Xml.Linq; @@ -59,7 +61,7 @@ namespace Barotrauma.Items.Components { MoveInputQueue(); - if (inputContainer == null || inputContainer.Inventory.Items.All(i => i == null)) + if (inputContainer == null || inputContainer.Inventory.IsEmpty()) { SetActive(false); return; @@ -79,7 +81,7 @@ namespace Barotrauma.Items.Components if (powerConsumption <= 0.0f) { Voltage = 1.0f; } progressTimer += deltaTime * Math.Min(Voltage, 1.0f); - var targetItem = inputContainer.Inventory.Items.LastOrDefault(i => i != null); + var targetItem = inputContainer.Inventory.LastOrDefault(); if (targetItem == null) { return; } float deconstructTime = targetItem.Prefab.DeconstructItems.Any() ? targetItem.Prefab.DeconstructTime / DeconstructionSpeed : 1.0f; @@ -87,78 +89,107 @@ namespace Barotrauma.Items.Components progressState = Math.Min(progressTimer / deconstructTime, 1.0f); if (progressTimer > deconstructTime) { - int emptySlots = outputContainer.Inventory.Items.Where(i => i == null).Count(); + // In multiplayer, the server handles the deconstruction into new items + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } - foreach (DeconstructItem deconstructProduct in targetItem.Prefab.DeconstructItems) + if (targetItem.Prefab.RandomDeconstructionOutput) + { + int amount = targetItem.Prefab.RandomDeconstructionOutputAmount; + List deconstructItemIndexes = new List(); + for (int i = 0; i < targetItem.Prefab.DeconstructItems.Count; i++) + { + deconstructItemIndexes.Add(i); + } + List commonness = targetItem.Prefab.DeconstructItems.Select(i => i.Commonness).ToList(); + List products = new List(); + + for (int i = 0; i < amount; i++) + { + if (deconstructItemIndexes.Count < 1) { break; } + var itemIndex = ToolBox.SelectWeightedRandom(deconstructItemIndexes, commonness, Rand.RandSync.Unsynced); + products.Add(targetItem.Prefab.DeconstructItems[itemIndex]); + var removeIndex = deconstructItemIndexes.IndexOf(itemIndex); + deconstructItemIndexes.RemoveAt(removeIndex); + commonness.RemoveAt(removeIndex); + } + foreach (DeconstructItem deconstructProduct in products) + { + CreateDeconstructProduct(deconstructProduct); + } + } + else + { + foreach (DeconstructItem deconstructProduct in targetItem.Prefab.DeconstructItems) + { + CreateDeconstructProduct(deconstructProduct); + } + } + + void CreateDeconstructProduct(DeconstructItem deconstructProduct) { float percentageHealth = targetItem.Condition / targetItem.Prefab.Health; - if (percentageHealth <= deconstructProduct.MinCondition || percentageHealth > deconstructProduct.MaxCondition) continue; + if (percentageHealth <= deconstructProduct.MinCondition || percentageHealth > deconstructProduct.MaxCondition) { return; } if (!(MapEntityPrefab.Find(null, deconstructProduct.ItemIdentifier) is ItemPrefab itemPrefab)) { DebugConsole.ThrowError("Tried to deconstruct item \"" + targetItem.Name + "\" but couldn't find item prefab \"" + deconstructProduct.ItemIdentifier + "\"!"); - continue; + return; } float condition = deconstructProduct.CopyCondition ? percentageHealth * itemPrefab.Health : itemPrefab.Health * deconstructProduct.OutCondition; - //container full, drop the items outside the deconstructor - if (emptySlots <= 0) + Entity.Spawner.AddToSpawnQueue(itemPrefab, outputContainer.Inventory, condition, onSpawned: (Item spawnedItem) => { - Entity.Spawner.AddToSpawnQueue(itemPrefab, item.Position, item.Submarine, condition); - } - else - { - Entity.Spawner.AddToSpawnQueue(itemPrefab, outputContainer.Inventory, condition); - emptySlots--; - } - } - - if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) - { - if (targetItem.Prefab.AllowDeconstruct) - { - //drop all items that are inside the deconstructed item - foreach (ItemContainer ic in targetItem.GetComponents()) + for (int i = 0; i < outputContainer.Capacity; i++) { - if (ic?.Inventory?.Items == null || ic.RemoveContainedItemsOnDeconstruct) { continue; } - foreach (Item containedItem in ic.Inventory.Items) + var containedItem = outputContainer.Inventory.GetItemAt(i); + if (containedItem?.Combine(spawnedItem, null) ?? false) { - containedItem?.Drop(dropper: null, createNetworkEvent: true); + break; } } - - inputContainer.Inventory.RemoveItem(targetItem); - Entity.Spawner.AddToRemoveQueue(targetItem); - MoveInputQueue(); PutItemsToLinkedContainer(); + }); + } + + if (targetItem.Prefab.AllowDeconstruct) + { + //drop all items that are inside the deconstructed item + foreach (ItemContainer ic in targetItem.GetComponents()) + { + if (ic?.Inventory == null || ic.RemoveContainedItemsOnDeconstruct) { continue; } + ic.Inventory.AllItemsMod.ForEach(containedItem => outputContainer.Inventory.TryPutItem(containedItem, user: null)); + } + inputContainer.Inventory.RemoveItem(targetItem); + Entity.Spawner.AddToRemoveQueue(targetItem); + MoveInputQueue(); + PutItemsToLinkedContainer(); + } + else + { + if (!outputContainer.Inventory.CanBePut(targetItem)) + { + targetItem.Drop(dropper: null); } else { - if (outputContainer.Inventory.Items.All(i => i != null)) - { - targetItem.Drop(dropper: null); - } - else - { - outputContainer.Inventory.TryPutItem(targetItem, user: null, createNetworkEvent: true); - } + outputContainer.Inventory.TryPutItem(targetItem, user: null, createNetworkEvent: true); } -#if SERVER - item.CreateServerEvent(this); -#endif - progressTimer = 0.0f; - progressState = 0.0f; } +#if SERVER + item.CreateServerEvent(this); +#endif + progressTimer = 0.0f; + progressState = 0.0f; } } private void PutItemsToLinkedContainer() { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } - if (outputContainer.Inventory.Items.All(it => it == null)) return; + if (outputContainer.Inventory.IsEmpty()) { return; } foreach (MapEntity linkedTo in item.linkedTo) { @@ -168,13 +199,7 @@ namespace Barotrauma.Items.Components if (fabricator != null) { continue; } var itemContainer = linkedItem.GetComponent(); if (itemContainer == null) { continue; } - - foreach (Item containedItem in outputContainer.Inventory.Items) - { - if (containedItem == null) { continue; } - if (itemContainer.Inventory.Items.All(it => it != null)) { break; } - itemContainer.Inventory.TryPutItem(containedItem, user: null, createNetworkEvent: true); - } + outputContainer.Inventory.AllItemsMod.ForEach(containedItem => itemContainer.Inventory.TryPutItem(containedItem, user: null, createNetworkEvent: true)); } } } @@ -186,9 +211,12 @@ namespace Barotrauma.Items.Components { for (int i = inputContainer.Inventory.Capacity - 2; i >= 0; i--) { - if (inputContainer.Inventory.Items[i] != null && inputContainer.Inventory.Items[i + 1] == null) + while (inputContainer.Inventory.GetItemAt(i) is Item item1 && inputContainer.Inventory.CanBePut(item1, i + 1)) { - inputContainer.Inventory.TryPutItem(inputContainer.Inventory.Items[i], i + 1, allowSwapping: false, allowCombine: false, user: null, createNetworkEvent: true); + if (!inputContainer.Inventory.TryPutItem(item1, i + 1, allowSwapping: false, allowCombine: false, user: null, createNetworkEvent: true)) + { + break; + } } } } @@ -197,18 +225,16 @@ namespace Barotrauma.Items.Components { PutItemsToLinkedContainer(); - if (inputContainer.Inventory.Items.All(i => i == null)) { active = false; } + if (inputContainer.Inventory.IsEmpty()) { active = false; } IsActive = active; currPowerConsumption = IsActive ? powerConsumption : 0.0f; - #if SERVER if (user != null) { GameServer.Log(GameServer.CharacterLogName(user) + (IsActive ? " activated " : " deactivated ") + item.Name, ServerLog.MessageType.ItemInteraction); } #endif - if (!IsActive) { progressTimer = 0.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs index f69095eeb..eb947a5ae 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs @@ -27,7 +27,7 @@ namespace Barotrauma.Items.Components public Character User; [Editable(0.0f, 10000000.0f), - Serialize(2000.0f, true, description: "The amount of force exerted on the submarine when the engine is operating at full power.")] + Serialize(500.0f, true, description: "The amount of force exerted on the submarine when the engine is operating at full power.")] public float MaxForce { get { return maxForce; } @@ -46,6 +46,13 @@ namespace Barotrauma.Items.Components set; } + [Editable, Serialize(false, true)] + public bool DisablePropellerDamage + { + get; + set; + } + public float Force { get { return force;} @@ -117,6 +124,7 @@ namespace Barotrauma.Items.Components Vector2 currForce = new Vector2(force * maxForce * forceMultiplier * voltageFactor, 0.0f); //less effective when in a bad condition currForce *= MathHelper.Lerp(0.5f, 2.0f, item.Condition / item.MaxCondition); + if (item.Submarine.FlippedX) { currForce *= -1; } item.Submarine.ApplyForce(currForce); UpdatePropellerDamage(deltaTime); float maxChangeSpeed = 0.5f; @@ -130,7 +138,7 @@ namespace Barotrauma.Items.Components if (particleTimer <= 0.0f) { Vector2 particleVel = -currForce.ClampLength(5000.0f) / 5.0f; - GameMain.ParticleManager.CreateParticle("bubbles", item.WorldPosition + PropellerPos, + GameMain.ParticleManager.CreateParticle("bubbles", item.WorldPosition + PropellerPos * item.Scale, particleVel * Rand.Range(0.9f, 1.1f), 0.0f, item.CurrentHull); particleTimer = 1.0f / particlesPerSec; @@ -154,19 +162,22 @@ namespace Barotrauma.Items.Components private void UpdatePropellerDamage(float deltaTime) { + if (DisablePropellerDamage) { return; } + damageTimer += deltaTime; - if (damageTimer < 0.5f) return; + if (damageTimer < 0.5f) { return; } damageTimer = 0.1f; - if (propellerDamage == null) return; - Vector2 propellerWorldPos = item.WorldPosition + PropellerPos; + if (propellerDamage == null) { return; } + + float scaledDamageRange = propellerDamage.DamageRange * item.Scale; + + Vector2 propellerWorldPos = item.WorldPosition + PropellerPos * item.Scale; foreach (Character character in Character.CharacterList) { - if (character.Submarine != null || !character.Enabled || character.Removed) continue; - - float dist = Vector2.DistanceSquared(character.WorldPosition, propellerWorldPos); - if (dist > propellerDamage.DamageRange * propellerDamage.DamageRange) continue; - + if (character.Submarine != null || !character.Enabled || character.Removed) { continue; } + float distSqr = Vector2.DistanceSquared(character.WorldPosition, propellerWorldPos); + if (distSqr > scaledDamageRange * scaledDamageRange) { continue; } character.LastDamageSource = item; propellerDamage.DoDamage(null, character, propellerWorldPos, 1.0f, true); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index 0e8e1d5e9..dd3f19214 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -142,7 +142,7 @@ namespace Barotrauma.Items.Components public override bool Pick(Character picker) { - return (picker != null); + return picker != null; } public void RemoveFabricationRecipes(List allowedIdentifiers) @@ -161,10 +161,10 @@ namespace Barotrauma.Items.Components partial void CreateRecipes(); - private void StartFabricating(FabricationRecipe selectedItem, Character user) + private void StartFabricating(FabricationRecipe selectedItem, Character user, bool addToServerLog = true) { if (selectedItem == null) { return; } - if (!outputContainer.Inventory.IsEmpty()) { return; } + if (!outputContainer.Inventory.CanBePut(selectedItem.TargetItem)) { return; } #if CLIENT itemList.Enabled = false; @@ -190,7 +190,7 @@ namespace Barotrauma.Items.Components State = FabricatorState.Active; } #if SERVER - if (user != null) + if (user != null && addToServerLog) { GameServer.Log(GameServer.CharacterLogName(user) + " started fabricating " + selectedItem.DisplayName + " in " + item.Name, ServerLog.MessageType.ItemInteraction); } @@ -298,20 +298,24 @@ namespace Barotrauma.Items.Components } Character tempUser = user; - if (outputContainer.Inventory.Items.All(i => i != null)) + int amountFittingContainer = outputContainer.Inventory.HowManyCanBePut(fabricatedItem.TargetItem); + for (int i = 0; i < fabricatedItem.Amount; i++) { - Entity.Spawner.AddToSpawnQueue(fabricatedItem.TargetItem, item.Position, item.Submarine, fabricatedItem.TargetItem.Health * fabricatedItem.OutCondition, - onSpawned: (Item spawnedItem) => { onItemSpawned(spawnedItem, tempUser); }); - } - else - { - Entity.Spawner.AddToSpawnQueue(fabricatedItem.TargetItem, outputContainer.Inventory, fabricatedItem.TargetItem.Health * fabricatedItem.OutCondition, - onSpawned: (Item spawnedItem) => { onItemSpawned(spawnedItem, tempUser); }); + if (i < amountFittingContainer) + { + Entity.Spawner.AddToSpawnQueue(fabricatedItem.TargetItem, outputContainer.Inventory, fabricatedItem.TargetItem.Health * fabricatedItem.OutCondition, + onSpawned: (Item spawnedItem) => { onItemSpawned(spawnedItem, tempUser); }); + } + else + { + Entity.Spawner.AddToSpawnQueue(fabricatedItem.TargetItem, item.Position, item.Submarine, fabricatedItem.TargetItem.Health * fabricatedItem.OutCondition, + onSpawned: (Item spawnedItem) => { onItemSpawned(spawnedItem, tempUser); }); + } } static void onItemSpawned(Item spawnedItem, Character user) { - if (user != null && user.TeamID != Character.TeamType.None) + if (user != null && user.TeamID != CharacterTeamType.None) { foreach (WifiComponent wifiComponent in spawnedItem.GetComponents()) { @@ -328,10 +332,23 @@ namespace Barotrauma.Items.Components user.Info.IncreaseSkillLevel( skill.Identifier, skill.Level * SkillSettings.Current.SkillIncreasePerFabricatorRequiredSkill / Math.Max(userSkill, 1.0f), - user.WorldPosition + Vector2.UnitY * 150.0f); + user.Position + Vector2.UnitY * 150.0f); } } + //disabled "continuous fabrication" for now + //before we enable it, there should be some UI controls for fabricating a specific number of items + + /*var prevFabricatedItem = fabricatedItem; + var prevUser = user; + CancelFabricating(); + if (CanBeFabricated(prevFabricatedItem)) + { + //keep fabricating if we can fabricate more + StartFabricating(prevFabricatedItem, prevUser, addToServerLog: false); + }*/ + + CancelFabricating(); } } @@ -375,7 +392,7 @@ namespace Barotrauma.Items.Components float skillSum = (from t in skills let characterLevel = character.GetSkillLevel(t.Identifier) select (characterLevel - (t.Level * SkillRequirementMultiplier))).Sum(); float average = skillSum / skills.Count; - return ((average + 100.0f) / 2.0f) / 100.0f; + return (average + 100.0f) / 2.0f / 100.0f; } public override float GetSkillMultiplier() @@ -390,7 +407,7 @@ namespace Barotrauma.Items.Components private List GetAvailableIngredients() { List availableIngredients = new List(); - availableIngredients.AddRange(inputContainer.Inventory.Items.Where(it => it != null)); + availableIngredients.AddRange(inputContainer.Inventory.AllItems); foreach (MapEntity linkedTo in item.linkedTo) { if (linkedTo is Item linkedItem) @@ -404,18 +421,18 @@ namespace Barotrauma.Items.Components itemContainer = deconstructor.OutputContainer; } - availableIngredients.AddRange(itemContainer.Inventory.Items.Where(it => it != null)); + availableIngredients.AddRange(itemContainer.Inventory.AllItems); } } #if CLIENT if (Character.Controlled?.Inventory != null) { - availableIngredients.AddRange(Character.Controlled.Inventory.Items.Distinct().Where(it => it != null)); + availableIngredients.AddRange(Character.Controlled.Inventory.AllItems); } #else if (user?.Inventory != null) { - availableIngredients.AddRange(user.Inventory.Items.Distinct().Where(it => it != null)); + availableIngredients.AddRange(user.Inventory.AllItems); } #endif @@ -450,9 +467,9 @@ namespace Barotrauma.Items.Components } else //in another inventory, we need to move the item { - if (inputContainer.Inventory.Items.All(it => it != null)) + if (!inputContainer.Inventory.CanBePut(matchingItem)) { - var unneededItem = inputContainer.Inventory.Items.FirstOrDefault(it => !usedItems.Contains(it)); + var unneededItem = inputContainer.Inventory.AllItems.FirstOrDefault(it => !usedItems.Contains(it)); unneededItem?.Drop(null, createNetworkEvent: !isClient); } inputContainer.Inventory.TryPutItem(matchingItem, user: null, createNetworkEvent: !isClient); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs index fda5537bb..a8d8edf58 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs @@ -102,7 +102,7 @@ namespace Barotrauma.Items.Components if (item.CurrentHull == null) { return; } - float powerFactor = Math.Min(currPowerConsumption <= 0.0f ? 1.0f : Voltage, 1.0f); + float powerFactor = Math.Min(currPowerConsumption <= 0.0f || MinVoltage <= 0.0f ? 1.0f : Voltage, 1.0f); currFlow = flowPercentage / 100.0f * maxFlow * powerFactor; //less effective when in a bad condition @@ -112,13 +112,16 @@ namespace Barotrauma.Items.Components if (item.CurrentHull.WaterVolume > item.CurrentHull.Volume) { item.CurrentHull.Pressure += 0.5f; } } - public void InfectBallast(string identifier) + public void InfectBallast(string identifier, bool allowMultiplePerShip = false) { Hull hull = item.CurrentHull; if (hull == null) { return; } - // if the ship is already infected then do nothing - if (Hull.hullList.Where(h => h.Submarine == hull.Submarine).Any(h => h.BallastFlora != null)) { return; } + if (!allowMultiplePerShip) + { + // if the ship is already infected then do nothing + if (Hull.hullList.Where(h => h.Submarine == hull.Submarine).Any(h => h.BallastFlora != null)) { return; } + } if (hull.BallastFlora != null) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs index 1c923ba90..a769590c8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs @@ -216,7 +216,7 @@ namespace Barotrauma.Items.Components if (LastAIUser.SelectedConstruction != item && LastAIUser.CanInteractWith(item)) { AutoTemp = true; - unsentChanges = true; + if (GameMain.NetworkMember?.IsServer ?? false) { unsentChanges = true; } LastAIUser = null; } } @@ -313,12 +313,11 @@ namespace Barotrauma.Items.Components if (fissionRate > 0.0f) { - var containedItems = item.OwnInventory?.Items; + var containedItems = item.OwnInventory?.AllItems; if (containedItems != null) { foreach (Item item in containedItems) { - if (item == null) { continue; } if (!item.HasTag("reactorfuel")) { continue; } item.Condition -= fissionRate / 100.0f * fuelConsumptionRate * deltaTime; } @@ -414,8 +413,8 @@ namespace Barotrauma.Items.Components private bool TooMuchFuel() { - var containedItems = item.OwnInventory?.Items; - if (containedItems != null && containedItems.Count(i => i != null) <= 1) { return false; } + var containedItems = item.OwnInventory?.AllItems; + if (containedItems != null && containedItems.Count() <= 1) { return false; } //get the amount of heat we'd generate if the fission rate was at the low end of the optimal range float minimumHeat = GetGeneratedHeat(optimalFissionRate.X); @@ -532,12 +531,11 @@ namespace Barotrauma.Items.Components fireTimer = 0.0f; meltDownTimer = 0.0f; - var containedItems = item.OwnInventory?.Items; + var containedItems = item.OwnInventory?.AllItems; if (containedItems != null) { foreach (Item containedItem in containedItems) { - if (containedItem == null) { continue; } containedItem.Condition = 0.0f; } } @@ -559,55 +557,58 @@ namespace Barotrauma.Items.Components public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; } + bool shutDown = objective.Option.Equals("shutdown", StringComparison.OrdinalIgnoreCase); IsActive = true; - float degreeOfSuccess = DegreeOfSuccess(character); - float refuelLimit = 0.3f; - //characters with insufficient skill levels don't refuel the reactor - if (degreeOfSuccess > refuelLimit) + if (!shutDown) { - if (objective.SubObjectives.None()) + float degreeOfSuccess = DegreeOfSuccess(character); + float refuelLimit = 0.3f; + //characters with insufficient skill levels don't refuel the reactor + if (degreeOfSuccess > refuelLimit) { - if (!AIDecontainEmptyItems(character, objective, equip: false)) - { - return false; - } - } - - if (aiUpdateTimer > 0.0f) - { - aiUpdateTimer -= deltaTime; - return false; - } - aiUpdateTimer = AIUpdateInterval; - - // load more fuel if the current maximum output is only 50% of the current load - // or if the fuel rod is (almost) deplenished - float minCondition = fuelConsumptionRate * MathUtils.Pow((degreeOfSuccess - refuelLimit) * 2, 2); - if (NeedMoreFuel(minimumOutputRatio: 0.5f, minCondition: minCondition)) - { - var container = item.GetComponent(); if (objective.SubObjectives.None()) { - int itemCount = item.ContainedItems.Count(i => i != null && container.ContainableItems.Any(ri => ri.MatchesItem(i))) + 1; - AIContainItems(container, character, objective, itemCount, equip: false, removeEmpty: true, spawnItemIfNotFound: character.TeamID == Character.TeamType.FriendlyNPC, dropItemOnDeselected: true); - character.Speak(TextManager.Get("DialogReactorFuel"), null, 0.0f, "reactorfuel", 30.0f); - } - return false; - } - else if (TooMuchFuel()) - { - var container = item.GetComponent(); - var containedItems = item.OwnInventory?.Items; - if (containedItems != null) - { - foreach (Item item in containedItems) + if (!AIDecontainEmptyItems(character, objective, equip: false)) { - if (item != null && container.ContainableItems.Any(ri => ri.MatchesItem(item))) + return false; + } + } + + if (aiUpdateTimer > 0.0f) + { + aiUpdateTimer -= deltaTime; + return false; + } + aiUpdateTimer = AIUpdateInterval; + + // load more fuel if the current maximum output is only 50% of the current load + // or if the fuel rod is (almost) deplenished + float minCondition = fuelConsumptionRate * MathUtils.Pow((degreeOfSuccess - refuelLimit) * 2, 2); + if (NeedMoreFuel(minimumOutputRatio: 0.5f, minCondition: minCondition)) + { + var container = item.GetComponent(); + if (objective.SubObjectives.None()) + { + int itemCount = item.ContainedItems.Count(i => i != null && container.ContainableItems.Any(ri => ri.MatchesItem(i))) + 1; + AIContainItems(container, character, objective, itemCount, equip: false, removeEmpty: true, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC, dropItemOnDeselected: true); + character.Speak(TextManager.Get("DialogReactorFuel"), null, 0.0f, "reactorfuel", 30.0f); + } + return false; + } + else if (TooMuchFuel()) + { + if (item.OwnInventory?.AllItems != null) + { + var container = item.GetComponent(); + foreach (Item item in item.OwnInventory.AllItemsMod) { - item.Drop(character); - break; + if (container.ContainableItems.Any(ri => ri.MatchesItem(item))) + { + item.Drop(character); + break; + } } } } @@ -624,7 +625,7 @@ namespace Barotrauma.Items.Components } } } - else if (LastUserWasPlayer) + else if (LastUserWasPlayer && lastUser != null && lastUser.TeamID == character.TeamID) { return true; } @@ -636,48 +637,45 @@ namespace Barotrauma.Items.Components float prevFissionRate = targetFissionRate; float prevTurbineOutput = targetTurbineOutput; - switch (objective.Option.ToLowerInvariant()) - { - case "powerup": - PowerOn = true; - if (objective.Override || !autoTemp) - { - //characters with insufficient skill levels simply set the autotemp on instead of trying to adjust the temperature manually - if (degreeOfSuccess < 0.5f) - { - AutoTemp = true; - } - else - { - AutoTemp = false; - UpdateAutoTemp(MathHelper.Lerp(0.5f, 2.0f, degreeOfSuccess), 1.0f); - } - } -#if CLIENT - FissionRateScrollBar.BarScroll = FissionRate / 100.0f; - TurbineOutputScrollBar.BarScroll = TurbineOutput / 100.0f; -#endif - break; - case "shutdown": - PowerOn = false; - AutoTemp = false; - targetFissionRate = 0.0f; - targetTurbineOutput = 0.0f; - unsentChanges = true; - return true; - } - - if (autoTemp != prevAutoTemp || - prevPowerOn != _powerOn || - Math.Abs(prevFissionRate - targetFissionRate) > 1.0f || - Math.Abs(prevTurbineOutput - targetTurbineOutput) > 1.0f) + if (shutDown) { + PowerOn = false; + AutoTemp = false; + targetFissionRate = 0.0f; + targetTurbineOutput = 0.0f; unsentChanges = true; + return true; + } + else + { + PowerOn = true; + if (objective.Override || !autoTemp) + { + //characters with insufficient skill levels simply set the autotemp on instead of trying to adjust the temperature manually + if (degreeOfSuccess < 0.5f) + { + AutoTemp = true; + } + else + { + AutoTemp = false; + UpdateAutoTemp(MathHelper.Lerp(0.5f, 2.0f, degreeOfSuccess), 1.0f); + } + } +#if CLIENT + FissionRateScrollBar.BarScroll = FissionRate / 100.0f; + TurbineOutputScrollBar.BarScroll = TurbineOutput / 100.0f; +#endif + if (autoTemp != prevAutoTemp || + prevPowerOn != _powerOn || + Math.Abs(prevFissionRate - targetFissionRate) > 1.0f || + Math.Abs(prevTurbineOutput - targetTurbineOutput) > 1.0f) + { + unsentChanges = true; + } + aiUpdateTimer = AIUpdateInterval; + return false; } - - aiUpdateTimer = AIUpdateInterval; - - return false; } public override void OnMapLoaded() @@ -696,14 +694,14 @@ namespace Barotrauma.Items.Components AutoTemp = false; targetFissionRate = 0.0f; targetTurbineOutput = 0.0f; - unsentChanges = true; + if (GameMain.NetworkMember?.IsServer ?? false) { unsentChanges = true; } } break; case "set_fissionrate": if (PowerOn && float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float newFissionRate)) { targetFissionRate = newFissionRate; - unsentChanges = true; + if (GameMain.NetworkMember?.IsServer ?? false) { unsentChanges = true; } #if CLIENT FissionRateScrollBar.BarScroll = targetFissionRate / 100.0f; #endif @@ -713,7 +711,7 @@ namespace Barotrauma.Items.Components if (PowerOn && float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float newTurbineOutput)) { targetTurbineOutput = newTurbineOutput; - unsentChanges = true; + if (GameMain.NetworkMember?.IsServer ?? false) { unsentChanges = true; } #if CLIENT TurbineOutputScrollBar.BarScroll = targetTurbineOutput / 100.0f; #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs index 663e9c815..404318043 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs @@ -22,7 +22,6 @@ namespace Barotrauma.Items.Components private const float AutoPilotMaxSpeed = 0.5f; private const float AIPilotMaxSpeed = 1.0f; - private Vector2 currVelocity; private Vector2 targetVelocity; private Vector2 steeringInput; @@ -52,7 +51,13 @@ namespace Barotrauma.Items.Components private Sonar sonar; private Submarine controlledSub; - + + private bool showIceSpireWarning; + + private List connectedSubs = new List(); + private const float ConnectedSubUpdateInterval = 1.0f; + float connectedSubUpdateTimer; + public bool AutoPilot { get { return autoPilot; } @@ -302,6 +307,7 @@ namespace Barotrauma.Items.Components } else { + showIceSpireWarning = false; if (user != null && user.Info != null && user.SelectedConstruction == item && controlledSub != null && controlledSub.Velocity.LengthSquared() > 0.01f) @@ -326,12 +332,13 @@ namespace Barotrauma.Items.Components } } } - - item.SendSignal(0, targetVelocity.X.ToString(CultureInfo.InvariantCulture), "velocity_x_out", user); - float targetLevel = -targetVelocity.Y; + float targetLevel = targetVelocity.X; + if (controlledSub != null && controlledSub.FlippedX) { targetLevel *= -1; } + item.SendSignal(0, targetLevel.ToString(CultureInfo.InvariantCulture), "velocity_x_out", user); + + targetLevel = -targetVelocity.Y; targetLevel += (neutralBallastLevel - 0.5f) * 100.0f; - item.SendSignal(0, targetLevel.ToString(CultureInfo.InvariantCulture), "velocity_y_out", user); } @@ -345,7 +352,7 @@ namespace Barotrauma.Items.Components user.Info.IncreaseSkillLevel( "helm", SkillSettings.Current.SkillIncreasePerSecondWhenSteering / userSkill * deltaTime, - user.WorldPosition + Vector2.UnitY * 150.0f); + user.Position + Vector2.UnitY * 150.0f); } private void UpdateAutoPilot(float deltaTime) @@ -354,7 +361,8 @@ namespace Barotrauma.Items.Components if (posToMaintain != null) { Vector2 steeringVel = GetSteeringVelocity((Vector2)posToMaintain, 10.0f); - TargetVelocity = Vector2.Lerp(TargetVelocity, steeringVel, AutoPilotSteeringLerp); + TargetVelocity = Vector2.Lerp(TargetVelocity, steeringVel, AutoPilotSteeringLerp); + showIceSpireWarning = false; return; } @@ -368,9 +376,21 @@ namespace Barotrauma.Items.Components autopilotRecalculatePathTimer = RecalculatePathInterval; } - if (steeringPath == null) { return; } + if (steeringPath == null) + { + showIceSpireWarning = false; + return; + } steeringPath.CheckProgress(ConvertUnits.ToSimUnits(controlledSub.WorldPosition), 10.0f); + connectedSubUpdateTimer -= deltaTime; + if (connectedSubUpdateTimer <= 0.0f) + { + connectedSubs.Clear(); + connectedSubs = controlledSub?.GetConnectedSubs(); + connectedSubUpdateTimer = ConnectedSubUpdateInterval; + } + if (autopilotRayCastTimer <= 0.0f && steeringPath.NextNode != null) { Vector2 diff = ConvertUnits.ToSimUnits(steeringPath.NextNode.Position - controlledSub.WorldPosition); @@ -420,13 +440,14 @@ namespace Barotrauma.Items.Components Math.Max(1000.0f * Math.Abs(controlledSub.Velocity.Y), controlledSub.Borders.Height * 0.75f)); float avoidRadius = avoidDist.Length(); - float damagingWallAvoidRadius = avoidRadius * 1.5f; + float damagingWallAvoidRadius = MathHelper.Clamp(avoidRadius * 1.5f, 5000.0f, 10000.0f); Vector2 newAvoidStrength = Vector2.Zero; debugDrawObstacles.Clear(); //steer away from nearby walls + showIceSpireWarning = false; var closeCells = Level.Loaded.GetCells(controlledSub.WorldPosition, 4); foreach (VoronoiCell cell in closeCells) { @@ -435,12 +456,22 @@ namespace Barotrauma.Items.Components foreach (GraphEdge edge in cell.Edges) { Vector2 closestPoint = MathUtils.GetClosestPointOnLineSegment(edge.Point1 + cell.Translation, edge.Point2 + cell.Translation, controlledSub.WorldPosition); - float dist = Vector2.Distance(closestPoint, controlledSub.WorldPosition); + Vector2 diff = closestPoint - controlledSub.WorldPosition; + float dist = diff.Length() - Math.Max(controlledSub.Borders.Width, controlledSub.Borders.Height) / 2; if (dist > damagingWallAvoidRadius) { continue; } - Vector2 diff = controlledSub.WorldPosition - cell.Center; - Vector2 avoid = Vector2.Normalize(diff) * (damagingWallAvoidRadius - dist) / damagingWallAvoidRadius; + + Vector2 normalizedDiff = Vector2.Normalize(diff); + float dot = Vector2.Dot(normalizedDiff, controlledSub.Velocity); + + float avoidStrength = MathHelper.Clamp(MathHelper.Lerp(1.0f, 0.0f, dist / damagingWallAvoidRadius - dot), 0.0f, 1.0f); + Vector2 avoid = -normalizedDiff * avoidStrength; newAvoidStrength += avoid; debugDrawObstacles.Add(new ObstacleDebugInfo(edge, edge.Center, 1.0f, avoid, cell.Translation)); + + if (dot > 0.0f) + { + showIceSpireWarning = true; + } } continue; } @@ -456,7 +487,7 @@ namespace Barotrauma.Items.Components debugDrawObstacles.Add(new ObstacleDebugInfo(edge, intersection, 0.0f, Vector2.Zero, Vector2.Zero)); continue; } - if (diff.LengthSquared() < 1.0f) diff = Vector2.UnitY; + if (diff.LengthSquared() < 1.0f) { diff = Vector2.UnitY; } Vector2 normalizedDiff = Vector2.Normalize(diff); float dot = controlledSub.Velocity == Vector2.Zero ? @@ -483,8 +514,7 @@ namespace Barotrauma.Items.Components //steer away from other subs foreach (Submarine sub in Submarine.Loaded) { - if (sub == controlledSub) { continue; } - if (controlledSub.DockedTo.Contains(sub)) { continue; } + if (sub == controlledSub || connectedSubs.Contains(sub)) { continue; } Point sizeSum = controlledSub.Borders.Size + sub.Borders.Size; Vector2 minDist = sizeSum.ToVector2() / 2; Vector2 diff = controlledSub.WorldPosition - sub.WorldPosition; @@ -659,6 +689,10 @@ namespace Barotrauma.Items.Components break; } sonar?.AIOperate(deltaTime, character, objective); + if (!MaintainPos && showIceSpireWarning) + { + character.Speak(TextManager.Get("dialogicespirespottedsonar"), null, 0.0f, "icespirespottedsonar", 60.0f); + } return false; } @@ -666,7 +700,7 @@ namespace Barotrauma.Items.Components { if (connection.Name == "velocity_in") { - currVelocity = XMLExtensions.ParseVector2(signal, false); + TargetVelocity = XMLExtensions.ParseVector2(signal, errorMessages: false); } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/NameTag.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/NameTag.cs index 97c8acae8..25eebb6a9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/NameTag.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/NameTag.cs @@ -4,7 +4,7 @@ namespace Barotrauma.Items.Components { class NameTag : ItemComponent { - [InGameEditable, Serialize("", false, description: "Name written on the tag.", alwaysUseInstanceValues: true)] + [InGameEditable(MaxLength = 32), Serialize("", false, description: "Name written on the tag.", alwaysUseInstanceValues: true)] public string WrittenName { get; set; } public NameTag(Item item, XElement element) : base(item, element) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs index d9d66f667..35de12b51 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Planter.cs @@ -74,6 +74,8 @@ namespace Barotrauma.Items.Components [Serialize(100f, true, "How much fertilizer can the planter hold.")] public float FertilizerCapacity { get; set; } + public string LastAction { get; set; } = ""; + public Growable?[] GrowableSeeds = new Growable?[0]; private readonly List SuitableFertilizer = new List(); @@ -168,6 +170,8 @@ namespace Barotrauma.Items.Components switch (plantItem.Type) { case PlantItemType.Seed: + LastAction = "PlantSeed"; + ApplyStatusEffects(ActionType.OnPicked, 1.0f, character); return container.Inventory.TryPutItem(plantItem.Item, character, new List { InvSlotType.Any }); case PlantItemType.Fertilizer when plantItem.Item != null: float canAdd = FertilizerCapacity - Fertilizer; @@ -178,6 +182,8 @@ namespace Barotrauma.Items.Components #if CLIENT character.UpdateHUDProgressBar(this, Item.DrawPosition, Fertilizer / FertilizerCapacity, Color.SaddleBrown, Color.SaddleBrown, "entityname.fertilizer"); #endif + LastAction = "ApplyFertilizer"; + ApplyStatusEffects(ActionType.OnPicked, 1.0f, character); return false; } @@ -203,6 +209,8 @@ namespace Barotrauma.Items.Components container?.Inventory.RemoveItem(seed.Item); Entity.Spawner?.AddToRemoveQueue(seed.Item); GrowableSeeds[i] = null; + LastAction = "Harvest"; + ApplyStatusEffects(ActionType.OnPicked, 1.0f, character); return true; } } @@ -226,12 +234,11 @@ namespace Barotrauma.Items.Components if (container?.Inventory == null) { return; } - for (var i = 0; i < container.Inventory.Items.Length; i++) + for (var i = 0; i < container.Inventory.Capacity; i++) { if (i < 0 || GrowableSeeds.Length <= i) { continue; } - Item containedItem = container.Inventory.Items[i]; - + Item containedItem = container.Inventory.GetItemAt(i); Growable? growable = containedItem?.GetComponent(); if (growable != null) @@ -289,11 +296,9 @@ namespace Barotrauma.Items.Components private SuitablePlantItem GetSuitableItem(Character character) { - foreach (Item heldItem in character.SelectedItems) + foreach (Item heldItem in character.HeldItems) { - if (heldItem == null) { continue; } - - if (container?.Inventory != null && !container.Inventory.IsFull()) + if (container?.Inventory != null && container.Inventory.CanBePut(heldItem)) { if (heldItem.GetComponent() != null && SuitableSeeds.Any(ri => ri.MatchesItem(heldItem))) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs index 344c81281..1db23da24 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs @@ -131,7 +131,7 @@ namespace Barotrauma.Items.Components { if (!powerOnSoundPlayed && powerOnSound != null) { - SoundPlayer.PlaySound(powerOnSound.Sound, item.WorldPosition, powerOnSound.Volume, powerOnSound.Range, hullGuess: item.CurrentHull); + SoundPlayer.PlaySound(powerOnSound.Sound, item.WorldPosition, powerOnSound.Volume, powerOnSound.Range, hullGuess: item.CurrentHull, ignoreMuffling: powerOnSound.IgnoreMuffling); powerOnSoundPlayed = true; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index d51f61eec..56fa478d5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -559,7 +559,7 @@ namespace Barotrauma.Items.Components item.body.SimPosition - ConvertUnits.ToSimUnits(sub.Position) - dir, item.body.SimPosition - ConvertUnits.ToSimUnits(sub.Position) + dir, collisionCategory: Physics.CollisionWall); - if (wallBody?.FixtureList?.First() != null && wallBody.UserData is Structure structure && + if (wallBody?.FixtureList?.First() != null && wallBody.UserData is Structure && //ignore the hit if it's behind the position the item was launched from, and the projectile is travelling in the opposite direction Vector2.Dot(item.body.SimPosition - launchPos, dir) > 0) { @@ -656,8 +656,14 @@ namespace Barotrauma.Items.Components if (character != null) { character.LastDamageSource = item; } + ActionType actionType = ActionType.OnUse; + if (_user != null && Rand.Range(0.0f, 0.5f) > DegreeOfSuccess(_user)) + { + actionType = ActionType.OnFailure; + } + #if CLIENT - PlaySound(ActionType.OnUse, user: _user); + PlaySound(actionType, user: _user); PlaySound(ActionType.OnImpact, user: _user); #endif @@ -665,7 +671,7 @@ namespace Barotrauma.Items.Components { if (target.Body.UserData is Limb targetLimb) { - ApplyStatusEffects(ActionType.OnUse, 1.0f, character, targetLimb, user: _user); + ApplyStatusEffects(actionType, 1.0f, character, targetLimb, user: _user); ApplyStatusEffects(ActionType.OnImpact, 1.0f, character, targetLimb, user: _user); var attack = targetLimb.attack; if (attack != null) @@ -695,19 +701,19 @@ namespace Barotrauma.Items.Components #if SERVER if (GameMain.NetworkMember.IsServer) { - GameMain.Server?.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnUse, this, targetLimb.character.ID, targetLimb, (ushort)0, item.WorldPosition }); + GameMain.Server?.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, actionType, this, targetLimb.character.ID, targetLimb, (ushort)0, item.WorldPosition }); GameMain.Server?.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnImpact, this, targetLimb.character.ID, targetLimb, (ushort)0, item.WorldPosition }); } #endif } else { - ApplyStatusEffects(ActionType.OnUse, 1.0f, useTarget: target.Body.UserData as Entity, user: _user); + ApplyStatusEffects(actionType, 1.0f, useTarget: target.Body.UserData as Entity, user: _user); ApplyStatusEffects(ActionType.OnImpact, 1.0f, useTarget: target.Body.UserData as Entity, user: _user); #if SERVER if (GameMain.NetworkMember.IsServer) { - GameMain.Server?.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnUse, this, (ushort)0, null, (target.Body.UserData as Entity)?.ID ?? 0, item.WorldPosition }); + GameMain.Server?.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, actionType, this, (ushort)0, null, (target.Body.UserData as Entity)?.ID ?? 0, item.WorldPosition }); GameMain.Server?.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnImpact, this, (ushort)0, null, (target.Body.UserData as Entity)?.ID ?? 0, item.WorldPosition }); } #endif @@ -764,12 +770,11 @@ namespace Barotrauma.Items.Components item.body.LinearVelocity *= 0.5f; } - var containedItems = item.OwnInventory?.Items; + var containedItems = item.OwnInventory?.AllItems; if (containedItems != null) { foreach (Item contained in containedItems) { - if (contained == null) { continue; } if (contained.body != null) { contained.SetTransform(item.SimPosition, contained.body.Rotation); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs index fd1ad5456..d50f09265 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs @@ -351,7 +351,7 @@ namespace Barotrauma.Items.Components float characterSkillLevel = CurrentFixer.GetSkillLevel(skill.Identifier); CurrentFixer.Info.IncreaseSkillLevel(skill.Identifier, SkillSettings.Current.SkillIncreasePerRepair / Math.Max(characterSkillLevel, 1.0f), - CurrentFixer.WorldPosition + Vector2.UnitY * 100.0f); + CurrentFixer.Position + Vector2.UnitY * 100.0f); } SteamAchievementManager.OnItemRepaired(item, CurrentFixer); } @@ -381,7 +381,7 @@ namespace Barotrauma.Items.Components float characterSkillLevel = CurrentFixer.GetSkillLevel(skill.Identifier); CurrentFixer.Info.IncreaseSkillLevel(skill.Identifier, SkillSettings.Current.SkillIncreasePerSabotage / Math.Max(characterSkillLevel, 1.0f), - CurrentFixer.WorldPosition + Vector2.UnitY * 100.0f); + CurrentFixer.Position + Vector2.UnitY * 100.0f); } deteriorationTimer = 0.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ArithmeticComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ArithmeticComponent.cs index ab81a6d70..a2705ec91 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ArithmeticComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ArithmeticComponent.cs @@ -52,15 +52,18 @@ namespace Barotrauma.Items.Components sealed public override void Update(float deltaTime, Camera cam) { + bool deactivate = true; + bool earlyReturn = false; for (int i = 0; i < timeSinceReceived.Length; i++) { - if (timeSinceReceived[i] > timeFrame) - { - IsActive = false; - return; - } + deactivate &= timeSinceReceived[i] > timeFrame; + earlyReturn |= timeSinceReceived[i] > timeFrame; timeSinceReceived[i] += deltaTime; } + // only stop Update() if both signals timed-out. if IsActive == false, then the component stops updating. + IsActive = !deactivate; + // early return if either of the signal timed-out + if (earlyReturn) { return; } float output = Calculate(receivedSignal[0], receivedSignal[1]); if (MathUtils.IsValid(output)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConcatComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConcatComponent.cs index ab56fdf18..4870ffd67 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConcatComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConcatComponent.cs @@ -1,9 +1,23 @@ using System.Xml.Linq; +using System; namespace Barotrauma.Items.Components { class ConcatComponent : StringComponent { + private int maxOutputLength; + + [Editable, Serialize(256, false, description: "The maximum length of the output string. Warning: Large values can lead to large memory usage or networking load.")] + public int MaxOutputLength + { + get { return maxOutputLength; } + set + { + maxOutputLength = Math.Max(value, 0); + } + } + + public ConcatComponent(Item item, XElement element) : base(item, element) { @@ -11,7 +25,8 @@ namespace Barotrauma.Items.Components protected override string Calculate(string signal1, string signal2) { - return signal1 + signal2; + string output = signal1 + signal2; + return output.Length <= maxOutputLength ? output : output.Substring(0, MaxOutputLength); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs index f10a2540f..d40506693 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs @@ -8,13 +8,16 @@ namespace Barotrauma.Items.Components { partial class Connection { - //how many wires can be linked to a single connector - public const int MaxLinked = 5; + //how many wires can be linked to connectors by default + private const int DefaultMaxWires = 5; + + //how many wires can be linked to this connection + public readonly int MaxWires = 5; public readonly string Name; public readonly string DisplayName; - private Wire[] wires; + private readonly Wire[] wires; public IEnumerable Wires { get { return wires; } @@ -77,7 +80,8 @@ namespace Barotrauma.Items.Components ConnectionPanel = connectionPanel; item = connectionPanel.Item; - wires = new Wire[MaxLinked]; + MaxWires = element.GetAttributeInt("maxwires", DefaultMaxWires); + wires = new Wire[MaxWires]; IsOutput = element.Name.ToString() == "output"; Name = element.GetAttributeString("name", IsOutput ? "output" : "input"); @@ -135,7 +139,7 @@ namespace Barotrauma.Items.Components Effects = new List(); - wireId = new ushort[MaxLinked]; + wireId = new ushort[MaxWires]; foreach (XElement subElement in element.Elements()) { @@ -143,7 +147,7 @@ namespace Barotrauma.Items.Components { case "link": int index = -1; - for (int i = 0; i < MaxLinked; i++) + for (int i = 0; i < MaxWires; i++) { if (wireId[i] < 1) index = i; } @@ -173,7 +177,7 @@ namespace Barotrauma.Items.Components private void RefreshRecipients() { recipients.Clear(); - for (int i = 0; i < MaxLinked; i++) + for (int i = 0; i < MaxWires; i++) { if (wires[i] == null) continue; Connection recipient = wires[i].OtherConnection(this); @@ -184,7 +188,7 @@ namespace Barotrauma.Items.Components public int FindEmptyIndex() { - for (int i = 0; i < MaxLinked; i++) + for (int i = 0; i < MaxWires; i++) { if (wires[i] == null) return i; } @@ -193,7 +197,7 @@ namespace Barotrauma.Items.Components public int FindWireIndex(Wire wire) { - for (int i = 0; i < MaxLinked; i++) + for (int i = 0; i < MaxWires; i++) { if (wires[i] == wire) return i; } @@ -202,7 +206,7 @@ namespace Barotrauma.Items.Components public int FindWireIndex(Item wireItem) { - for (int i = 0; i < MaxLinked; i++) + for (int i = 0; i < MaxWires; i++) { if (wires[i] == null && wireItem == null) return i; if (wires[i] != null && wires[i].Item == wireItem) return i; @@ -212,7 +216,7 @@ namespace Barotrauma.Items.Components public bool TryAddLink(Wire wire) { - for (int i = 0; i < MaxLinked; i++) + for (int i = 0; i < MaxWires; i++) { if (wires[i] == null) { @@ -250,7 +254,7 @@ namespace Barotrauma.Items.Components public void SendSignal(int stepsTaken, string signal, Item source, Character sender, float power, float signalStrength = 1.0f) { - for (int i = 0; i < MaxLinked; i++) + for (int i = 0; i < MaxWires; i++) { if (wires[i] == null) { continue; } @@ -274,7 +278,7 @@ namespace Barotrauma.Items.Components public void SendPowerProbeSignal(Item source, float power) { - for (int i = 0; i < MaxLinked; i++) + for (int i = 0; i < MaxWires; i++) { if (wires[i] == null) { continue; } @@ -286,7 +290,7 @@ namespace Barotrauma.Items.Components } public void ClearConnections() { - for (int i = 0; i < MaxLinked; i++) + for (int i = 0; i < MaxWires; i++) { if (wires[i] == null) continue; @@ -300,7 +304,7 @@ namespace Barotrauma.Items.Components { if (wireId == null) return; - for (int i = 0; i < MaxLinked; i++) + for (int i = 0; i < MaxWires; i++) { if (wireId[i] == 0) { continue; } @@ -329,7 +333,7 @@ namespace Barotrauma.Items.Components return wire1.Item.ID.CompareTo(wire2.Item.ID); }); - for (int i = 0; i < MaxLinked; i++) + for (int i = 0; i < MaxWires; i++) { if (wires[i] == null) continue; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs index 2dc21d449..7cf83ea93 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs @@ -1,7 +1,5 @@ using Barotrauma.Networking; -using FarseerPhysics; using Microsoft.Xna.Framework; -using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; @@ -111,7 +109,7 @@ namespace Barotrauma.Items.Components public override void OnItemLoaded() { - if (item.body != null) + if (item.body != null && item.body.BodyType == FarseerPhysics.BodyType.Dynamic) { var holdable = item.GetComponent(); if (holdable == null || !holdable.Attachable) @@ -130,12 +128,12 @@ namespace Barotrauma.Items.Components { foreach (Wire wire in c.Wires) { - if (wire == null) continue; + if (wire == null) { continue; } #if CLIENT - if (wire.Item.IsSelected) continue; + if (wire.Item.IsSelected) { continue; } #endif var wireNodes = wire.GetNodes(); - if (wireNodes.Count == 0) continue; + if (wireNodes.Count == 0) { continue; } if (Submarine.RectContains(item.Rect, wireNodes[0] + wireNodeOffset)) { @@ -184,7 +182,7 @@ namespace Barotrauma.Items.Components { //attaching wires to items with a body is not allowed //(signal items remove their bodies when attached to a wall) - if (item.body != null) + if (item.body != null && item.body.BodyType == FarseerPhysics.BodyType.Dynamic) { return false; } @@ -247,10 +245,32 @@ namespace Barotrauma.Items.Components for (int i = 0; i < loadedConnections.Count && i < Connections.Count; i++) { - loadedConnections[i].wireId.CopyTo(Connections[i].wireId, 0); + if (loadedConnections[i].wireId.Length == Connections[i].wireId.Length) + { + loadedConnections[i].wireId.CopyTo(Connections[i].wireId, 0); + } + else + { + //backwards compatibility when maximum number of wires has changed + foreach (ushort id in loadedConnections[i].wireId) + { + for (int j = 0; j < Connections[i].wireId.Length; j++) + { + if (Connections[i].wireId[j] == 0) + { + Connections[i].wireId[j] = id; + break; + } + } + } + } } disconnectedWireIds = element.GetAttributeUshortArray("disconnectedwires", new ushort[0]).ToList(); + for (int i = 0; i < disconnectedWireIds.Count; i++) + { + disconnectedWireIds[i] = idRemap.GetOffsetId(disconnectedWireIds[i]); + } } public override XElement Save(XElement parentElement) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs index c8eb11157..2642b0488 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs @@ -12,25 +12,72 @@ namespace Barotrauma.Items.Components public bool ContinuousSignal; public bool State; public string ConnectionName; - public string PropertyName; public Connection Connection; + [Serialize("", false, translationTextTag: "Label.", description: "The text displayed on this button/tickbox."), Editable] public string Label { get; set; } [Serialize("1", false, description: "The signal sent out when this button is pressed or this tickbox checked."), Editable] public string Signal { get; set; } + public string PropertyName { get; } + public bool TargetOnlyParentProperty { get; } + public int NumberInputMin { get; } + public int NumberInputMax { get; } + public const int DefaultNumberInputMin = 0, DefaultNumberInputMax = 99; + public bool IsIntegerInput { get; } + public bool HasPropertyName { get; } + public bool ShouldSetProperty { get; set; } + public string Name => "CustomInterfaceElement"; public Dictionary SerializableProperties { get; set; } public List StatusEffects = new List(); - public CustomInterfaceElement(XElement element) + /// + /// Pass the parent component to the constructor to access the serializable properties + /// for elements which change property values. + /// + public CustomInterfaceElement(XElement element, CustomInterface parent) { Label = element.GetAttributeString("text", ""); ConnectionName = element.GetAttributeString("connection", ""); PropertyName = element.GetAttributeString("propertyname", "").ToLowerInvariant(); - Signal = element.GetAttributeString("signal", "1"); + TargetOnlyParentProperty = element.GetAttributeBool("targetonlyparentproperty", false); + NumberInputMin = element.GetAttributeInt("min", DefaultNumberInputMin); + NumberInputMax = element.GetAttributeInt("max", DefaultNumberInputMax); + + HasPropertyName = !string.IsNullOrEmpty(PropertyName); + IsIntegerInput = HasPropertyName && element.Name.ToString().ToLowerInvariant() == "integerinput"; + + if (element.Attribute("signal") is XAttribute attribute) + { + Signal = attribute.Value; + ShouldSetProperty = HasPropertyName; + } + else if (HasPropertyName && parent != null) + { + if (TargetOnlyParentProperty) + { + if (parent.SerializableProperties.ContainsKey(PropertyName)) + { + Signal = parent.SerializableProperties[PropertyName].GetValue(parent) as string; + } + } + else + { + foreach (ISerializableEntity e in parent.item.AllPropertyObjects) + { + if (!e.SerializableProperties.ContainsKey(PropertyName)) { continue; } + Signal = e.SerializableProperties[PropertyName].GetValue(e) as string; + break; + } + } + } + else + { + Signal = "1"; + } foreach (XElement subElement in element.Elements()) { @@ -50,13 +97,14 @@ namespace Barotrauma.Items.Components set { if (value == null) { return; } - string[] splitValues = value == "" ? new string[0] : value.Split(','); if (customInterfaceElementList.Count > 0) { + string[] splitValues = value == "" ? new string[0] : value.Split(','); UpdateLabels(splitValues); } } } + private string[] signals; [Serialize("", true, description: "The signals sent when the buttons are pressed or the tickboxes checked, separated by commas.")] public string Signals @@ -67,34 +115,29 @@ namespace Barotrauma.Items.Components set { if (value == null) { return; } - string[] splitValues = value == "" ? new string[0] : value.Split(';'); if (customInterfaceElementList.Count > 0) { - signals = new string[customInterfaceElementList.Count]; - for (int i = 0; i < customInterfaceElementList.Count; i++) - { - signals[i] = i < splitValues.Length ? splitValues[i] : customInterfaceElementList[i].Signal; - customInterfaceElementList[i].Signal = signals[i]; - } + string[] splitValues = value == "" ? new string[0] : value.Split(';'); + UpdateSignals(splitValues); } } } public override bool RecreateGUIOnResolutionChange => true; - private List customInterfaceElementList = new List(); + private readonly List customInterfaceElementList = new List(); public CustomInterface(Item item, XElement element) : base(item, element) { - int i = 0; foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "button": case "textbox": - var button = new CustomInterfaceElement(subElement) + case "integerinput": + var button = new CustomInterfaceElement(subElement, this) { ContinuousSignal = false }; @@ -105,7 +148,7 @@ namespace Barotrauma.Items.Components customInterfaceElementList.Add(button); break; case "tickbox": - var tickBox = new CustomInterfaceElement(subElement) + var tickBox = new CustomInterfaceElement(subElement, this) { ContinuousSignal = true }; @@ -116,10 +159,9 @@ namespace Barotrauma.Items.Components customInterfaceElementList.Add(tickBox); break; } - i++; } IsActive = true; - InitProjSpecific(element); + InitProjSpecific(); Labels = element.GetAttributeString("labels", ""); Signals = element.GetAttributeString("signals", ""); } @@ -142,6 +184,47 @@ namespace Barotrauma.Items.Components UpdateLabelsProjSpecific(); } + private void UpdateSignals(string[] newSignals) + { + signals = new string[customInterfaceElementList.Count]; + for (int i = 0; i < customInterfaceElementList.Count; i++) + { + var element = customInterfaceElementList[i]; + if (i < newSignals.Length) + { + var newSignal = newSignals[i]; + signals[i] = newSignal; + element.ShouldSetProperty = element.Signal != newSignal; + element.Signal = newSignal; + } + else + { + signals[i] = element.Signal; + } + + if (element.HasPropertyName && element.ShouldSetProperty) + { + if (element.TargetOnlyParentProperty) + { + if (SerializableProperties.ContainsKey(element.PropertyName)) + { + SerializableProperties[element.PropertyName].TrySetValue(this, element.Signal); + } + } + else + { + foreach (var po in item.AllPropertyObjects) + { + if (!po.SerializableProperties.ContainsKey(element.PropertyName)) { continue; } + po.SerializableProperties[element.PropertyName].TrySetValue(po, element.Signal); + } + } + customInterfaceElementList[i].ShouldSetProperty = false; + } + } + UpdateSignalsProjSpecific(); + } + public override void OnItemLoaded() { foreach (CustomInterfaceElement ciElement in customInterfaceElementList) @@ -152,7 +235,9 @@ namespace Barotrauma.Items.Components partial void UpdateLabelsProjSpecific(); - partial void InitProjSpecific(XElement element); + partial void UpdateSignalsProjSpecific(); + + partial void InitProjSpecific(); private void ButtonClicked(CustomInterfaceElement btnElement) { @@ -175,14 +260,38 @@ namespace Barotrauma.Items.Components private void TextChanged(CustomInterfaceElement textElement, string text) { + if (textElement == null) { return; } textElement.Signal = text; - foreach (ISerializableEntity e in item.AllPropertyObjects) + if (!textElement.TargetOnlyParentProperty) { - if (e.SerializableProperties.ContainsKey(textElement.PropertyName)) + foreach (ISerializableEntity e in item.AllPropertyObjects) { + if (!e.SerializableProperties.ContainsKey(textElement.PropertyName)) { continue; } e.SerializableProperties[textElement.PropertyName].TrySetValue(e, text); } - } + } + else if (SerializableProperties.ContainsKey(textElement.PropertyName)) + { + SerializableProperties[textElement.PropertyName].TrySetValue(this, text); + } + } + + private void ValueChanged(CustomInterfaceElement numberInputElement, int value) + { + if (numberInputElement == null) { return; } + numberInputElement.Signal = value.ToString(); + if (!numberInputElement.TargetOnlyParentProperty) + { + foreach (ISerializableEntity e in item.AllPropertyObjects) + { + if (!e.SerializableProperties.ContainsKey(numberInputElement.PropertyName)) { continue; } + e.SerializableProperties[numberInputElement.PropertyName].TrySetValue(e, value); + } + } + else if (SerializableProperties.ContainsKey(numberInputElement.PropertyName)) + { + SerializableProperties[numberInputElement.PropertyName].TrySetValue(this, value); + } } public override void Update(float deltaTime, Camera cam) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs index 361be3884..d2d0920e7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs @@ -25,6 +25,8 @@ namespace Barotrauma.Items.Components public PhysicsBody ParentBody; + private Turret turret; + [Serialize(100.0f, true, description: "The range of the emitted light. Higher values are more performance-intensive.", alwaysUseInstanceValues: true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2048.0f)] public float Range @@ -214,7 +216,14 @@ namespace Barotrauma.Items.Components IsActive = IsOn; item.AddTag("light"); } - + + public override void OnItemLoaded() + { + base.OnItemLoaded(); + SetLightSourceState(IsActive, lightBrightness); + turret = item.GetComponent(); + } + public override void Update(float deltaTime, Camera cam) { if (item.AiTarget != null) @@ -232,9 +241,19 @@ namespace Barotrauma.Items.Components return; } #if CLIENT - light.Position = ParentBody != null ? ParentBody.Position : item.Position; + if (ParentBody != null) + { + light.Position = ParentBody.Position; + } + else if (turret != null) + { + light.Position = new Vector2(item.Rect.X + turret.TransformedBarrelPos.X, item.Rect.Y - turret.TransformedBarrelPos.Y); + } + else + { + light.Position = item.Position; + } #endif - PhysicsBody body = ParentBody ?? item.body; if (body != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MemoryComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MemoryComponent.cs index 8d1c64ced..ea33ee0d0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MemoryComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MemoryComponent.cs @@ -49,6 +49,7 @@ namespace Barotrauma.Items.Components } break; case "signal_store": + case "lock_state": writeable = signal == "1"; break; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs index 9f10c0973..99eff26f6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs @@ -15,17 +15,24 @@ namespace Barotrauma.Items.Components private float updateTimer; + public enum TargetType + { + Any, + Human, + Monster + } + [Serialize(false, false, description: "Has the item currently detected movement. Intended to be used by StatusEffect conditionals (setting this value in XML has no effect).")] public bool MotionDetected { get; set; } - [Editable, Serialize(false, true, description: "Should the sensor only detect the movement of humans?", alwaysUseInstanceValues: true)] - public bool OnlyHumans + [InGameEditable, Serialize(TargetType.Any, true, description: "Which kind of targets can trigger the sensor?", alwaysUseInstanceValues: true)] + public TargetType Target { get; set; } - [Editable, Serialize(false, true, description: "Should the sensor ignore the bodies of dead characters?", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize(false, true, description: "Should the sensor ignore the bodies of dead characters?", alwaysUseInstanceValues: true)] public bool IgnoreDead { get; @@ -55,7 +62,7 @@ namespace Barotrauma.Items.Components } } - [Editable, Serialize("0,0", true, description: "The position to detect the movement at relative to the item. For example, 0,100 would detect movement 100 units above the item.")] + [InGameEditable, Serialize("0,0", true, description: "The position to detect the movement at relative to the item. For example, 0,100 would detect movement 100 units above the item.")] public Vector2 DetectOffset { get { return detectOffset; } @@ -80,7 +87,6 @@ namespace Barotrauma.Items.Components set; } - public MotionSensor(Item item, XElement element) : base(item, element) { @@ -93,6 +99,16 @@ namespace Barotrauma.Items.Components } } + public override void Load(XElement componentElement, bool usePrefabValues, IdRemap idRemap) + { + base.Load(componentElement, usePrefabValues, idRemap); + //backwards compatibility + if (componentElement.GetAttributeBool("onlyhumans", false)) + { + Target = TargetType.Human; + } + } + public override void Update(float deltaTime, Camera cam) { string signalOut = MotionDetected ? Output : FalseOutput; @@ -121,7 +137,16 @@ namespace Barotrauma.Items.Components foreach (Character c in Character.CharacterList) { if (IgnoreDead && c.IsDead) { continue; } - if (OnlyHumans && !c.IsHuman) { 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 diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/NotComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/NotComponent.cs index 40137c737..a5758ce30 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/NotComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/NotComponent.cs @@ -4,16 +4,36 @@ namespace Barotrauma.Items.Components { class NotComponent : ItemComponent { + private bool signalReceived; + + private bool continuousOutput; + [Editable, Serialize(false, true, description: "When enabled, the component continuously outputs \"1\" when it's not receiving a signal.", alwaysUseInstanceValues: true)] + public bool ContinuousOutput + { + get { return continuousOutput; } + set { continuousOutput = IsActive = value; } + } + public NotComponent(Item item, XElement element) : base (item, element) { } + public override void Update(float deltaTime, Camera cam) + { + base.Update(deltaTime, cam); + if (!signalReceived) + { + item.SendSignal(0, "1", "signal_out", null, 0.0f); + } + signalReceived = false; + } + public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f) { - if (connection.Name != "signal_in") return; - - item.SendSignal(stepsTaken, signal == "0" ? "1" : "0", "signal_out", sender, 0.0f, source, signalStrength); + if (connection.Name != "signal_in") { return; } + item.SendSignal(stepsTaken, signal == "0" || signal == string.Empty ? "1" : "0", "signal_out", sender, 0.0f, source, signalStrength); + signalReceived = true; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs index 2cf176ad0..fb0b0ca46 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs @@ -23,7 +23,7 @@ namespace Barotrauma.Items.Components [InGameEditable, Serialize(false, true, description: "Should the component output a value of a capture group instead of a constant signal.", alwaysUseInstanceValues: true)] public bool UseCaptureGroup { get; set; } - [Serialize("0", true, description: "The signal this item outputs when the received signal does not match the regular expression.", alwaysUseInstanceValues: true)] + [InGameEditable, Serialize("0", true, description: "The signal this item outputs when the received signal does not match the regular expression.", alwaysUseInstanceValues: true)] public string FalseOutput { get; set; } [InGameEditable, Serialize(true, true, description: "Should the component keep sending the output even after it stops receiving a signal, or only send an output when it receives a signal.", alwaysUseInstanceValues: true)] diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs index 04e273922..8cbd60429 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs @@ -180,6 +180,7 @@ namespace Barotrauma.Items.Components } else if (connection.Name == "toggle") { + if (signal == "0") { return; } SetState(!IsOn, false); } else if (connection.Name == "set_state") diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs index 036ca394f..6730cdf94 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs @@ -24,8 +24,8 @@ namespace Barotrauma.Items.Components private int[] channelMemory = new int[ChannelMemorySize]; - [Serialize(Character.TeamType.None, true, description: "WiFi components can only communicate with components that have the same Team ID.", alwaysUseInstanceValues: true)] - public Character.TeamType TeamID { get; set; } + [Serialize(CharacterTeamType.None, true, description: "WiFi components can only communicate with components that have the same Team ID.", alwaysUseInstanceValues: true)] + public CharacterTeamType TeamID { get; set; } [Editable, Serialize(20000.0f, false, description: "How close the recipient has to be to receive a signal from this WiFi component.", alwaysUseInstanceValues: true)] public float Range diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index c86f55e85..8566b7331 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -262,24 +262,33 @@ namespace Barotrauma.Items.Components public override void OnMapLoaded() { base.OnMapLoaded(); - var lightComponents = item.GetComponents(); - if (lightComponents != null && lightComponents.Count() > 0) - { - lightComponent = lightComponents.FirstOrDefault(lc => lc.Parent == this); -#if CLIENT - if (lightComponent != null) - { - lightComponent.Parent = null; - lightComponent.Rotation = Rotation - MathHelper.ToRadians(item.Rotation); - lightComponent.Light.Rotation = -rotation; - } -#endif - } + FindLightComponent(); if (loadedRotationLimits.HasValue) { RotationLimits = loadedRotationLimits.Value; } if (loadedBaseRotation.HasValue) { BaseRotation = loadedBaseRotation.Value; } UpdateTransformedBarrelPos(); } + private void FindLightComponent() + { + foreach (LightComponent lc in item.GetComponents()) + { + if (lc?.Parent == this) + { + lightComponent = lc; + break; + } + } + +#if CLIENT + if (lightComponent != null) + { + lightComponent.Parent = null; + lightComponent.Rotation = Rotation - MathHelper.ToRadians(item.Rotation); + lightComponent.Light.Rotation = -rotation; + } +#endif + } + public override void Update(float deltaTime, Camera cam) { this.cam = cam; @@ -304,7 +313,11 @@ namespace Barotrauma.Items.Components UpdateProjSpecific(deltaTime); - if (minRotation == maxRotation) { return; } + if (MathUtils.NearlyEqual(minRotation, maxRotation)) + { + UpdateLightComponent(); + return; + } float targetMidDiff = MathHelper.WrapAngle(targetRotation - (minRotation + maxRotation) / 2.0f); @@ -326,7 +339,7 @@ namespace Barotrauma.Items.Components { user.Info.IncreaseSkillLevel("weapons", SkillSettings.Current.SkillIncreasePerSecondWhenOperatingTurret * deltaTime / Math.Max(user.GetSkillLevel("weapons"), 1.0f), - user.WorldPosition + Vector2.UnitY * 150.0f); + user.Position + Vector2.UnitY * 150.0f); } float rotMidDiff = MathHelper.WrapAngle(rotation - (minRotation + maxRotation) / 2.0f); @@ -371,6 +384,11 @@ namespace Barotrauma.Items.Components angularVelocity *= -0.5f; } + UpdateLightComponent(); + } + + private void UpdateLightComponent() + { if (lightComponent != null) { lightComponent.Rotation = Rotation - MathHelper.ToRadians(item.Rotation); @@ -709,7 +727,11 @@ namespace Barotrauma.Items.Components } var collisionCategories = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel; var pickedBody = Submarine.PickBody(start, end, null, collisionCategories, allowInsideFixture: true, - customPredicate: (Fixture f) => { return !item.StaticFixtures.Contains(f); }); + customPredicate: (Fixture f) => + { + if (f.UserData is Item i && i.GetComponent() != null) { return false; } + return !item.StaticFixtures.Contains(f); + }); if (pickedBody == null) { return; } Character targetCharacter = null; if (pickedBody.UserData is Character c) @@ -753,7 +775,7 @@ namespace Barotrauma.Items.Components if (character.AIController.SelectedAiTarget?.Entity is Character previousTarget && previousTarget.IsDead) { - character?.Speak(TextManager.Get("DialogTurretTargetDead"), null, 0.0f, "killedtarget" + previousTarget.ID, 30.0f); + character.Speak(TextManager.Get("DialogTurretTargetDead"), null, 0.0f, "killedtarget" + previousTarget.ID, 10.0f); character.AIController.SelectTarget(null); } @@ -765,7 +787,7 @@ namespace Barotrauma.Items.Components PowerContainer batteryToLoad = null; foreach (PowerContainer battery in batteries) { - if (battery.Item.NonInteractable) { continue; } + if (!battery.Item.IsInteractable(character)) { continue; } if (batteryToLoad == null || battery.Charge < lowestCharge) { batteryToLoad = battery; @@ -786,18 +808,14 @@ namespace Barotrauma.Items.Components int maxProjectileCount = 0; foreach (MapEntity e in item.linkedTo) { - if (item.NonInteractable) { continue; } + if (!item.IsInteractable(character)) { continue; } if (e is Item projectileContainer) { - var containedItems = projectileContainer.ContainedItems; - if (containedItems != null) - { - var container = projectileContainer.GetComponent(); - maxProjectileCount += container.Capacity; + var container = projectileContainer.GetComponent(); + maxProjectileCount += container.Capacity; - int projectiles = containedItems.Count(it => it.Condition > 0.0f); - usableProjectileCount += projectiles; - } + int projectiles = projectileContainer.ContainedItems.Count(it => it.Condition > 0.0f); + usableProjectileCount += projectiles; } } @@ -809,7 +827,7 @@ namespace Barotrauma.Items.Components { containerItem = e as Item; if (containerItem == null) { continue; } - if (containerItem.NonInteractable) { continue; } + if (!containerItem.IsInteractable(character)) { continue; } if (character.AIController is HumanAIController aiController && aiController.IgnoredItems.Contains(containerItem)) { continue; } container = containerItem.GetComponent(); if (container != null) { break; } @@ -852,53 +870,135 @@ namespace Barotrauma.Items.Components //enough shells and power Character closestEnemy = null; - float closestDist = AIRange * AIRange; + Vector2? targetPos = null; + float shootDistance = AIRange * item.OffsetOnSelectedMultiplier; + float closestDistance = shootDistance * shootDistance; foreach (Character enemy in Character.CharacterList) { // Ignore dead, friendly, and those that are inside the same sub if (enemy.IsDead || !enemy.Enabled || enemy.Submarine == character.Submarine) { continue; } - if (HumanAIController.IsFriendly(character, enemy)) { continue; } - + if (HumanAIController.IsFriendly(character, enemy)) { continue; } float dist = Vector2.DistanceSquared(enemy.WorldPosition, item.WorldPosition); - if (dist > closestDist) { continue; } - - float angle = -MathUtils.VectorToAngle(enemy.WorldPosition - item.WorldPosition); - float midRotation = (minRotation + maxRotation) / 2.0f; - while (midRotation - angle < -MathHelper.Pi) { angle -= MathHelper.TwoPi; } - while (midRotation - angle > MathHelper.Pi) { angle += MathHelper.TwoPi; } - - if (angle < minRotation || angle > maxRotation) { continue; } - + if (dist > closestDistance) { continue; } + if (!CheckTurretAngle(enemy.WorldPosition)) { continue; } closestEnemy = enemy; - closestDist = dist; + closestDistance = dist; } - if (closestEnemy == null) { return false; } - - character.AIController.SelectTarget(closestEnemy.AiTarget); + if (closestEnemy != null) + { + targetPos = closestEnemy.WorldPosition; + } + else if (item.Submarine != null && Level.Loaded != null) + { + // Check ice spires + closestDistance = shootDistance; + foreach (var wall in Level.Loaded.ExtraWalls) + { + if (!(wall is DestructibleLevelWall destructibleWall) || destructibleWall.Destroyed) { continue; } + foreach (var cell in wall.Cells) + { + if (cell.DoesDamage) + { + foreach (var edge in cell.Edges) + { + Vector2 p1 = edge.Point1 + cell.Translation; + Vector2 p2 = edge.Point2 + cell.Translation; + Vector2 closestPoint = MathUtils.GetClosestPointOnLineSegment(p1, p2, item.WorldPosition); + if (!CheckTurretAngle(closestPoint)) + { + // The closest point can't be targeted -> get a point directly in front of the turret + Vector2 barrelDir = new Vector2((float)Math.Cos(rotation), -(float)Math.Sin(rotation)); + if (MathUtils.GetLineIntersection(p1, p2, item.WorldPosition, item.WorldPosition + barrelDir * shootDistance, out Vector2 intersection)) + { + closestPoint = intersection; + if (!CheckTurretAngle(closestPoint)) { continue; } + } + else + { + continue; + } + } + float dist = Vector2.Distance(closestPoint, item.WorldPosition); + if (dist > AIRange + 1000) { continue; } + float dot = 0; + if (item.Submarine.Velocity != Vector2.Zero) + { + dot = Vector2.Dot(Vector2.Normalize(item.Submarine.Velocity), Vector2.Normalize(closestPoint - item.Submarine.WorldPosition)); + } + float minAngle = 0.5f; + if (dot < minAngle && dist > 1000) + { + // The sub is not moving towards the target and it's not very close to the turret either -> ignore + continue; + } + // Allow targeting farther when heading towards the spire (up to 1000 px) + dist -= MathHelper.Lerp(0, 1000, MathUtils.InverseLerp(minAngle, 1, dot)); ; + if (dist > closestDistance) { continue; } + targetPos = closestPoint; + closestDistance = dist; + } + } + } + } + } - character.CursorPosition = closestEnemy.WorldPosition; + if (targetPos == null) { return false; } + + if (closestEnemy != null && character.AIController.SelectedAiTarget != closestEnemy.AiTarget) + { + if (character.AIController.SelectedAiTarget == null) + { + if (GameMain.Config.RecentlyEncounteredCreatures.Contains(closestEnemy.SpeciesName)) + { + character.Speak(TextManager.Get("DialogNewTargetSpotted"), null, 0.0f, "newtargetspotted", 30.0f); + } + else if (GameMain.Config.EncounteredCreatures.Any(name => name.Equals(closestEnemy.SpeciesName, StringComparison.OrdinalIgnoreCase))) + { + character.Speak(TextManager.GetWithVariable("DialogIdentifiedTargetSpotted", "[speciesname]", closestEnemy.DisplayName), null, 0.0f, "identifiedtargetspotted", 30.0f); + } + else + { + character.Speak(TextManager.Get("DialogUnidentifiedTargetSpotted"), null, 0.0f, "unidentifiedtargetspotted", 5.0f); + } + } + else if (GameMain.Config.EncounteredCreatures.None(name => name.Equals(closestEnemy.SpeciesName, StringComparison.OrdinalIgnoreCase))) + { + character.Speak(TextManager.Get("DialogUnidentifiedTargetSpotted"), null, 0.0f, "unidentifiedtargetspotted", 5.0f); + } + character.AddEncounter(closestEnemy); + character.AIController.SelectTarget(closestEnemy.AiTarget); + } + else if (closestEnemy == null) + { + character.Speak(TextManager.Get("DialogIceSpireSpotted"), null, 0.0f, "icespirespotted", 60.0f); + } + + character.CursorPosition = targetPos.Value; if (character.Submarine != null) { character.CursorPosition -= character.Submarine.Position; } - float enemyAngle = MathUtils.VectorToAngle(closestEnemy.WorldPosition - item.WorldPosition); + float enemyAngle = MathUtils.VectorToAngle(targetPos.Value - item.WorldPosition); float turretAngle = -rotation; if (Math.Abs(MathUtils.GetShortestAngle(enemyAngle, turretAngle)) > 0.15f) { return false; } - Vector2 start = ConvertUnits.ToSimUnits(item.WorldPosition); - Vector2 end = ConvertUnits.ToSimUnits(closestEnemy.WorldPosition); - if (closestEnemy.Submarine != null) + Vector2 end = ConvertUnits.ToSimUnits(targetPos.Value); + if (closestEnemy != null && closestEnemy.Submarine != null) { start -= closestEnemy.Submarine.SimPosition; end -= closestEnemy.Submarine.SimPosition; } var collisionCategories = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel; var pickedBody = Submarine.PickBody(start, end, null, collisionCategories, allowInsideFixture: true, - customPredicate: (Fixture f) => { return !item.StaticFixtures.Contains(f); }); + customPredicate: (Fixture f) => + { + if (f.UserData is Item i && i.GetComponent() != null) { return false; } + return !item.StaticFixtures.Contains(f); + }); if (pickedBody == null) { return false; } Character targetCharacter = null; if (pickedBody.UserData is Character c) @@ -929,17 +1029,26 @@ namespace Barotrauma.Items.Components // Don't shoot friendly submarines. if (sub.TeamID == Item.Submarine.TeamID) { return false; } } - else + else if (!(pickedBody.UserData is Voronoi2.VoronoiCell cell && cell.IsDestructible)) { // Hit something else, probably a level wall return false; } } - character?.Speak(TextManager.GetWithVariable("DialogFireTurret", "[itemname]", item.Name, true), null, 0.0f, "fireturret", 5.0f); + character.Speak(TextManager.Get("DialogFireTurret"), null, 0.0f, "fireturret", 10.0f); character.SetInput(InputType.Shoot, true, true); return false; } + private bool CheckTurretAngle(Vector2 target) + { + float angle = -MathUtils.VectorToAngle(target - item.WorldPosition); + float midRotation = (minRotation + maxRotation) / 2.0f; + while (midRotation - angle < -MathHelper.Pi) { angle -= MathHelper.TwoPi; } + while (midRotation - angle > MathHelper.Pi) { angle += MathHelper.TwoPi; } + return angle > minRotation && angle < maxRotation; + } + protected override void RemoveComponentSpecific() { base.RemoveComponentSpecific(); @@ -984,7 +1093,6 @@ namespace Barotrauma.Items.Components else { //check if the contained item is another itemcontainer with projectiles inside it - if (containedItem.ContainedItems == null) { continue; } foreach (Item subContainedItem in containedItem.ContainedItems) { projectileComponent = subContainedItem.GetComponent(); @@ -1094,6 +1202,7 @@ namespace Barotrauma.Items.Components public override void OnItemLoaded() { base.OnItemLoaded(); + FindLightComponent(); if (!loadedBaseRotation.HasValue) { if (item.FlippedX) { FlipX(relativeToSub: false); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs index 6963fd045..b8b67e1b0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -1,4 +1,5 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; @@ -9,17 +10,209 @@ namespace Barotrauma { partial class Inventory : IServerSerializable, IClientSerializable { + public const int MaxStackSize = 32; + + public class ItemSlot + { + private readonly List items = new List(MaxStackSize); + + public bool HideIfEmpty; + + public IEnumerable Items + { + get { return items; } + } + + public int ItemCount + { + get { return items.Count; } + } + + public bool CanBePut(Item item) + { + if (item == null) { return false; } + if (items.Count > 0) + { + if (item.IsFullCondition) + { + if (items.Any(it => !it.IsFullCondition)) { return false; } + } + else if (MathUtils.NearlyEqual(item.Condition, 0.0f)) + { + if (items.Any(it => !MathUtils.NearlyEqual(it.Condition, 0.0f))) { return false; } + } + else + { + return false; + } + if (items[0].Prefab.Identifier != item.Prefab.Identifier || + items.Count + 1 > item.Prefab.MaxStackSize) + { + return false; + } + } + return true; + } + + public bool CanBePut(ItemPrefab itemPrefab) + { + if (itemPrefab == null) { return false; } + if (items.Count > 0) + { + if (items.Any(it => !it.IsFullCondition)) { return false; } + if (items[0].Prefab.Identifier != itemPrefab.Identifier || + items.Count + 1 > itemPrefab.MaxStackSize) + { + return false; + } + } + return true; + } + + /// Defaults to if null + public int HowManyCanBePut(ItemPrefab itemPrefab, int? maxStackSize = null) + { + if (itemPrefab == null) { return 0; } + maxStackSize ??= itemPrefab.MaxStackSize; + if (items.Count > 0) + { + if (items.Any(it => !it.IsFullCondition)) { return 0; } + if (items[0].Prefab.Identifier != itemPrefab.Identifier) { return 0; } + return maxStackSize.Value - items.Count; + } + else + { + return maxStackSize.Value; + } + } + + public void Add(Item item) + { + if (item == null) + { + throw new InvalidOperationException("Tried to add a null item to an inventory slot."); + } + if (items.Count > 0) + { + if (items[0].Prefab.Identifier != item.Prefab.Identifier) + { + throw new InvalidOperationException("Tried to stack different types of items."); + } + else if (items.Count + 1 > item.Prefab.MaxStackSize) + { + throw new InvalidOperationException("Tried to add an item to a full inventory slot (stack already full)."); + } + } + if (items.Contains(item)) { return; } + items.Add(item); + } + + /// + /// Removes one item from the slot + /// + public Item RemoveItem() + { + if (items.Count == 0) { return null; } + + var item = items[0]; + items.RemoveAt(0); + return item; + } + + public void RemoveItem(Item item) + { + items.Remove(item); + } + + /// + /// Removes all items from the slot + /// + public void RemoveAllItems() + { + items.Clear(); + } + + public bool Any() + { + return items.Count > 0; + } + + public bool Empty() + { + return items.Count == 0; + } + + public Item First() + { + return items[0]; + } + + public Item FirstOrDefault() + { + return items.FirstOrDefault(); + } + + public Item LastOrDefault() + { + return items.LastOrDefault(); + } + + public bool Contains(Item item) + { + return items.Contains(item); + } + + } + public readonly Entity Owner; protected readonly int capacity; - - public Item[] Items; - protected bool[] hideEmptySlot; + protected readonly ItemSlot[] slots; public bool Locked; protected float syncItemsDelay; + /// + /// All items contained in the inventory. Stacked items are returned as individual instances. DO NOT modify the contents of the inventory while enumerating this list. + /// + public IEnumerable AllItems + { + get + { + for (int i = 0; i < capacity; i++) + { + foreach (var item in slots[i].Items) + { + bool duplicateFound = false; + for (int j = 0; j < i; j++) + { + if (slots[j].Items.Contains(item)) + { + duplicateFound = true; + break; + } + } + if (!duplicateFound) { yield return item; } + } + } + } + } + + private readonly List allItemsList = new List(); + /// + /// All items contained in the inventory. Allows modifying the contents of the inventory while being enumerated. + /// + public IEnumerable AllItemsMod + { + get + { + allItemsList.Clear(); + allItemsList.AddRange(AllItems); + return allItemsList; + } + } + public int Capacity { get { return capacity; } @@ -31,8 +224,11 @@ namespace Barotrauma this.Owner = owner; - Items = new Item[capacity]; - hideEmptySlot = new bool[capacity]; + slots = new ItemSlot[capacity]; + for (int i = 0; i < capacity; i++) + { + slots[i] = new ItemSlot(); + } #if CLIENT this.slotsPerRow = slotsPerRow; @@ -54,71 +250,117 @@ namespace Barotrauma #endif } - public static Item FindItemRecursive(Item item, Predicate condition) + /// + /// Is the item contained in this inventory. Does not recursively check items inside items. + /// + public bool Contains(Item item) { - if (condition.Invoke(item)) + return slots.Any(i => i.Contains(item)); + } + + /// + /// Return the first item in the inventory, or null if the inventory is empty. + /// + public Item FirstOrDefault() + { + foreach (var itemSlot in slots) { - return item; + var item = itemSlot.FirstOrDefault(); + if (item != null) { return item; } } - - var containers = item.GetComponents(); - - if (containers != null) - { - foreach (var container in containers) - { - foreach (var inventoryItem in container.Inventory.Items) - { - var findItem = FindItemRecursive(inventoryItem, condition); - if (findItem != null) - { - return findItem; - } - } - } - } - return null; } + /// + /// Return the last item in the inventory, or null if the inventory is empty. + /// + public Item LastOrDefault() + { + for (int i = slots.Length - 1; i >= 0; i--) + { + var item = slots[i].LastOrDefault(); + if (item != null) { return item; } + } + return null; + } + + /// + /// Get the item stored in the specified inventory slot. If the slot contains a stack of items, returns the first item in the stack. + /// + public Item GetItemAt(int index) + { + if (index < 0 || index >= slots.Length) { return null; } + return slots[index].FirstOrDefault(); + } + + /// + /// Get all the item stored in the specified inventory slot. Can return more than one item if the slot contains a stack of items. + /// + public IEnumerable GetItemsAt(int index) + { + if (index < 0 || index >= slots.Length) { return Enumerable.Empty(); } + return slots[index].Items; + } + + /// + /// Find the index of the first slot the item is contained in. + /// public int FindIndex(Item item) { for (int i = 0; i < capacity; i++) { - if (Items[i] == item) return i; + if (slots[i].Contains(item)) { return i; } } return -1; } - - /// Returns true if the item owns any of the parent inventories + + /// + /// Find the indices of all the slots the item is contained in (two-hand items for example can be in multiple slots). Note that this method instantiates a new list. + /// + public List FindIndices(Item item) + { + List indices = new List(); + for (int i = 0; i < capacity; i++) + { + if (slots[i].Contains(item)) { indices.Add(i); } + } + return indices; + } + + /// + /// Returns true if the item owns any of the parent inventories. + /// public virtual bool ItemOwnsSelf(Item item) { - if (Owner == null) return false; - if (!(Owner is Item)) return false; + if (Owner == null) { return false; } + if (!(Owner is Item)) { return false; } Item ownerItem = Owner as Item; - if (ownerItem == item) return true; - if (ownerItem.ParentInventory == null) return false; + if (ownerItem == item) { return true; } + if (ownerItem.ParentInventory == null) { return false; } return ownerItem.ParentInventory.ItemOwnsSelf(item); } public virtual int FindAllowedSlot(Item item) { - if (ItemOwnsSelf(item)) return -1; + if (ItemOwnsSelf(item)) { return -1; } for (int i = 0; i < capacity; i++) { //item is already in the inventory! - if (Items[i] == item) return -1; + if (slots[i].Contains(item)) { return -1; } } for (int i = 0; i < capacity; i++) { - if (Items[i] == null) return i; + if (slots[i].CanBePut(item)) { return i; } } return -1; } + /// + /// Can the item be put in the inventory (i.e. is there a suitable free slot or a stack the item can be put in). + /// public bool CanBePut(Item item) { for (int i = 0; i < capacity; i++) @@ -128,20 +370,54 @@ namespace Barotrauma return false; } + /// + /// Can the item be put in the specified slot. + /// public virtual bool CanBePut(Item item, int i) { - if (ItemOwnsSelf(item)) return false; - if (i < 0 || i >= Items.Length) return false; - return (Items[i] == null); + if (ItemOwnsSelf(item)) { return false; } + if (i < 0 || i >= slots.Length) { return false; } + return slots[i].CanBePut(item); } - + + public bool CanBePut(ItemPrefab itemPrefab) + { + for (int i = 0; i < capacity; i++) + { + if (CanBePut(itemPrefab, i)) { return true; } + } + return false; + } + + public virtual bool CanBePut(ItemPrefab itemPrefab, int i) + { + if (i < 0 || i >= slots.Length) { return false; } + return slots[i].CanBePut(itemPrefab); + } + + public int HowManyCanBePut(ItemPrefab itemPrefab) + { + int count = 0; + for (int i = 0; i < capacity; i++) + { + count += HowManyCanBePut(itemPrefab, i); + } + return count; + } + + public virtual int HowManyCanBePut(ItemPrefab itemPrefab, int i) + { + if (i < 0 || i >= slots.Length) { return 0; } + return slots[i].HowManyCanBePut(itemPrefab); + } + /// /// If there is room, puts the item in the inventory and returns true, otherwise returns false /// - public virtual bool TryPutItem(Item item, Character user, List allowedSlots = null, bool createNetworkEvent = true) + public virtual bool TryPutItem(Item item, Character user, IEnumerable allowedSlots = null, bool createNetworkEvent = true) { int slot = FindAllowedSlot(item); - if (slot < 0) return false; + if (slot < 0) { return false; } PutItem(item, slot, user, true, createNetworkEvent); return true; @@ -149,7 +425,7 @@ namespace Barotrauma public virtual bool TryPutItem(Item item, int i, bool allowSwapping, bool allowCombine, Character user, bool createNetworkEvent = true) { - if (i < 0 || i >= Items.Length) + if (i < 0 || i >= slots.Length) { string errorMsg = "Inventory.TryPutItem failed: index was out of range(" + i + ").\n" + Environment.StackTrace.CleanupStackTrace(); GameAnalyticsManager.AddErrorEventOnce("Inventory.TryPutItem:IndexOutOfRange", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); @@ -158,31 +434,41 @@ namespace Barotrauma if (Owner == null) return false; //there's already an item in the slot - if (Items[i] != null && allowCombine) + if (slots[i].Any() && allowCombine) { - if (Items[i].Combine(item, user)) + if (slots[i].First().Combine(item, user)) { //item in the slot removed as a result of combining -> put this item in the now free slot - if (Items[i] == null) + if (!slots[i].Any()) { return TryPutItem(item, i, allowSwapping, allowCombine, user, createNetworkEvent); } return true; } } - if (Items[i] != null && item.ParentInventory != null && allowSwapping) - { - return TrySwapping(i, item, user, createNetworkEvent); - } - else if (CanBePut(item, i)) + if (CanBePut(item, i)) { PutItem(item, i, user, true, createNetworkEvent); return true; } + else if (slots[i].Any() && item.ParentInventory != null && allowSwapping) + { + var itemInSlot = slots[i].First(); + if (itemInSlot.OwnInventory != null && + !itemInSlot.OwnInventory.Contains(item) && + (itemInSlot.GetComponent()?.MaxStackSize ?? 0) == 1 && + itemInSlot.OwnInventory.TrySwapping(0, item, user, createNetworkEvent, swapWholeStack: false)) + { + return true; + } + return + TrySwapping(i, item, user, createNetworkEvent, swapWholeStack: true) || + TrySwapping(i, item, user, createNetworkEvent, swapWholeStack: false); + } else { #if CLIENT - if (slots != null && createNetworkEvent) slots[i].ShowBorderHighlight(GUI.Style.Red, 0.1f, 0.9f); + if (visualSlots != null && createNetworkEvent) { visualSlots[i].ShowBorderHighlight(GUI.Style.Red, 0.1f, 0.9f); } #endif return false; } @@ -190,14 +476,14 @@ namespace Barotrauma protected virtual void PutItem(Item item, int i, Character user, bool removeItem = true, bool createNetworkEvent = true) { - if (i < 0 || i >= Items.Length) + if (i < 0 || i >= slots.Length) { string errorMsg = "Inventory.PutItem failed: index was out of range(" + i + ").\n" + Environment.StackTrace.CleanupStackTrace(); GameAnalyticsManager.AddErrorEventOnce("Inventory.PutItem:IndexOutOfRange", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); return; } - if (Owner == null) return; + if (Owner == null) { return; } Inventory prevInventory = item.ParentInventory; Inventory prevOwnerInventory = item.FindParentInventory(inv => inv is CharacterInventory); @@ -206,20 +492,23 @@ namespace Barotrauma { CreateNetworkEvent(); //also delay syncing the inventory the item was inside - if (prevInventory != null && prevInventory != this) prevInventory.syncItemsDelay = 1.0f; + if (prevInventory != null && prevInventory != this) { prevInventory.syncItemsDelay = 1.0f; } } if (removeItem) { item.Drop(user); - if (item.ParentInventory != null) item.ParentInventory.RemoveItem(item); + if (item.ParentInventory != null) { item.ParentInventory.RemoveItem(item); } } - Items[i] = item; + slots[i].Add(item); item.ParentInventory = this; #if CLIENT - if (slots != null) slots[i].ShowBorderHighlight(Color.White, 0.1f, 0.4f); + if (visualSlots != null) + { + visualSlots[i]?.ShowBorderHighlight(Color.White, 0.1f, 0.4f); + } #endif if (item.body != null) @@ -258,33 +547,49 @@ namespace Barotrauma { for (int i = 0; i < capacity; i++) { - if (Items[i] != null) return false; + if (slots[i].Any()) { return false; } } return true; } - public bool IsFull() + /// + /// Is there room to put more items in the inventory. Doesn't take stacking into account by default. + /// + /// If true, the inventory is not considered full if all the stacks are not full. + public virtual bool IsFull(bool takeStacksIntoAccount = false) { - for (int i = 0; i < capacity; i++) + if (takeStacksIntoAccount) { - if (Items[i] == null) return false; + for (int i = 0; i < capacity; i++) + { + if (!slots[i].Any()) { return false; } + var item = slots[i].FirstOrDefault(); + if (slots[i].ItemCount < item.Prefab.MaxStackSize) { return false; } + } + } + else + { + for (int i = 0; i < capacity; i++) + { + if (!slots[i].Any()) { return false; } + } } return true; } - protected bool TrySwapping(int index, Item item, Character user, bool createNetworkEvent) + protected bool TrySwapping(int index, Item item, Character user, bool createNetworkEvent, bool swapWholeStack) { - if (item?.ParentInventory == null || Items[index] == null) return false; + if (item?.ParentInventory == null || !slots[index].Any()) { return false; } //swap to InvSlotType.Any if possible Inventory otherInventory = item.ParentInventory; bool otherIsEquipped = false; int otherIndex = -1; - for (int i = 0; i < otherInventory.Items.Length; i++) + for (int i = 0; i < otherInventory.slots.Length; i++) { - if (otherInventory.Items[i] != item) continue; + if (!otherInventory.slots[i].Contains(item)) { continue; } if (otherInventory is CharacterInventory characterInventory) { if (characterInventory.SlotTypes[i] == InvSlotType.Any) @@ -298,89 +603,138 @@ namespace Barotrauma } } } - - if (otherIndex == -1) otherIndex = Array.IndexOf(otherInventory.Items, item); - Item existingItem = Items[index]; - - for (int j = 0; j < otherInventory.capacity; j++) + if (otherIndex == -1) { - if (otherInventory.Items[j] == item) { otherInventory.Items[j] = null; } - } - for (int j = 0; j < capacity; j++) - { - if (Items[j] == existingItem) { Items[j] = null; } + otherIndex = otherInventory.FindIndex(item); + if (otherIndex == -1) + { + DebugConsole.ThrowError("Something went wrong when trying to swap items between inventory slots: couldn't find the source item from it's inventory.\n" + Environment.StackTrace.CleanupStackTrace()); + return false; + } + } + + List existingItems = new List(); + if (swapWholeStack) + { + existingItems.AddRange(slots[index].Items); + for (int j = 0; j < capacity; j++) + { + if (existingItems.Any(existingItem => slots[j].Contains(existingItem))) { slots[j].RemoveAllItems(); } + } + } + else + { + existingItems.Add(slots[index].FirstOrDefault()); + slots[index].RemoveItem(existingItems.First()); + } + + List stackedItems = new List(); + if (swapWholeStack) + { + for (int j = 0; j < otherInventory.capacity; j++) + { + if (otherInventory.slots[j].Contains(item)) + { + stackedItems.AddRange(otherInventory.slots[j].Items); + otherInventory.slots[j].RemoveAllItems(); + } + } + } + else + { + stackedItems.Add(item); + otherInventory.slots[otherIndex].RemoveItem(item); } - (otherInventory.Owner as Character)?.DeselectItem(item); - (otherInventory.Owner as Character)?.DeselectItem(existingItem); bool swapSuccessful = false; if (otherIsEquipped) { swapSuccessful = - TryPutItem(item, index, false, false, user, createNetworkEvent) && - otherInventory.TryPutItem(existingItem, otherIndex, false, false, user, createNetworkEvent); + stackedItems.Distinct().All(stackedItem => TryPutItem(stackedItem, index, false, false, user, createNetworkEvent)) + && + (existingItems.All(existingItem => otherInventory.TryPutItem(existingItem, otherIndex, false, false, user, createNetworkEvent)) || + existingItems.Count == 1 && otherInventory.TryPutItem(existingItems.First(), user, CharacterInventory.anySlot, createNetworkEvent)); } else { - swapSuccessful = - otherInventory.TryPutItem(existingItem, otherIndex, false, false, user, createNetworkEvent) && - TryPutItem(item, index, false, false, user, createNetworkEvent); + swapSuccessful = + (existingItems.All(existingItem => otherInventory.TryPutItem(existingItem, otherIndex, false, false, user, createNetworkEvent)) || + existingItems.Count == 1 && otherInventory.TryPutItem(existingItems.First(),user, CharacterInventory.anySlot, createNetworkEvent)) + && + stackedItems.Distinct().All(stackedItem => TryPutItem(stackedItem, index, false, false, user, createNetworkEvent)); } //if the item in the slot can be moved to the slot of the moved item if (swapSuccessful) { - System.Diagnostics.Debug.Assert(Items[index] == item, "Something when wrong when swapping items, item is not present in the inventory."); - System.Diagnostics.Debug.Assert(otherInventory.Items[otherIndex] == existingItem, "Something when wrong when swapping items, item is not present in the other inventory."); + System.Diagnostics.Debug.Assert(slots[index].Contains(item), "Something when wrong when swapping items, item is not present in the inventory."); + System.Diagnostics.Debug.Assert(otherInventory.Contains(existingItems.FirstOrDefault()), "Something when wrong when swapping items, item is not present in the other inventory."); #if CLIENT - if (slots != null) + if (visualSlots != null) { for (int j = 0; j < capacity; j++) { - if (Items[j] == item) slots[j].ShowBorderHighlight(GUI.Style.Green, 0.1f, 0.9f); + if (slots[j].Contains(item)) { visualSlots[j].ShowBorderHighlight(GUI.Style.Green, 0.1f, 0.9f); } } for (int j = 0; j < otherInventory.capacity; j++) { - if (otherInventory.Items[j] == existingItem) otherInventory.slots[j].ShowBorderHighlight(GUI.Style.Green, 0.1f, 0.9f); + if (otherInventory.slots[j].Contains(existingItems.FirstOrDefault())) { otherInventory.visualSlots[j].ShowBorderHighlight(GUI.Style.Green, 0.1f, 0.9f); } } } #endif return true; } - else + else //swapping the items failed -> move them back to where they were { - for (int j = 0; j < capacity; j++) + if (swapWholeStack) { - if (Items[j] == item) Items[j] = null; + foreach (Item stackedItem in stackedItems) + { + for (int j = 0; j < capacity; j++) + { + if (slots[j].Contains(stackedItem)) { slots[j].RemoveItem(stackedItem); }; + } + } + foreach (Item existingItem in existingItems) + { + for (int j = 0; j < otherInventory.capacity; j++) + { + if (otherInventory.slots[j].Contains(existingItem)) { otherInventory.slots[j].RemoveItem(existingItem); } + } + } } - for (int j = 0; j < otherInventory.capacity; j++) + else { - if (otherInventory.Items[j] == existingItem) otherInventory.Items[j] = null; + for (int j = 0; j < capacity; j++) + { + if (slots[j].Contains(item)) { slots[j].RemoveAllItems(); }; + } + for (int j = 0; j < otherInventory.capacity; j++) + { + if (otherInventory.slots[j].Contains(existingItems.FirstOrDefault())) { otherInventory.slots[j].RemoveAllItems(); } + } } if (otherIsEquipped) { - TryPutItem(existingItem, index, false, false, user, createNetworkEvent); - otherInventory.TryPutItem(item, otherIndex, false, false, user, createNetworkEvent); + existingItems.ForEach(existingItem => TryPutItem(existingItem, index, false, false, user, createNetworkEvent)); + stackedItems.ForEach(stackedItem => otherInventory.TryPutItem(stackedItem, otherIndex, false, false, user, createNetworkEvent)); } else { - otherInventory.TryPutItem(item, otherIndex, false, false, user, createNetworkEvent); - TryPutItem(existingItem, index, false, false, user, createNetworkEvent); + stackedItems.ForEach(stackedItem => otherInventory.TryPutItem(stackedItem, otherIndex, false, false, user, createNetworkEvent)); + existingItems.ForEach(existingItem => TryPutItem(existingItem, index, false, false, user, createNetworkEvent)); } - //swapping the items failed -> move them back to where they were - //otherInventory.TryPutItem(item, otherIndex, false, false, user, createNetworkEvent); - //TryPutItem(existingItem, index, false, false, user, createNetworkEvent); #if CLIENT - if (slots != null) + if (visualSlots != null) { for (int j = 0; j < capacity; j++) { - if (Items[j] == existingItem) + if (slots[j].Contains(existingItems.FirstOrDefault())) { - slots[j].ShowBorderHighlight(GUI.Style.Red, 0.1f, 0.9f); + visualSlots[j].ShowBorderHighlight(GUI.Style.Red, 0.1f, 0.9f); } } } @@ -400,10 +754,10 @@ namespace Barotrauma public Item FindItem(Func predicate, bool recursive) { - Item match = Items.FirstOrDefault(i => i != null && predicate(i)); + Item match = AllItems.FirstOrDefault(i => predicate(i)); if (match == null && recursive) { - foreach (var item in Items) + foreach (var item in AllItems) { if (item == null) { continue; } if (item.OwnInventory != null) @@ -422,9 +776,8 @@ namespace Barotrauma public List FindAllItems(Func predicate, bool recursive = false, List list = null) { list ??= new List(); - foreach (var item in Items) + foreach (var item in AllItems) { - if (item == null) { continue; } if (predicate(item)) { list.Add(item); @@ -448,30 +801,57 @@ namespace Barotrauma public Item FindItemByIdentifier(string identifier, bool recursive = false) { - if (identifier == null) return null; + if (identifier == null) { return null; } return FindItem(i => i.Prefab.Identifier == identifier, recursive); } public virtual void RemoveItem(Item item) { - if (item == null) return; + if (item == null) { return; } //go through the inventory and remove the item from all slots for (int n = 0; n < capacity; n++) { - if (Items[n] != item) continue; - - Items[n] = null; + if (!slots[n].Contains(item)) { continue; } + + slots[n].RemoveItem(item); item.ParentInventory = null; } } + /// + /// Forces an item to a specific slot. Doesn't remove the item from existing slots/inventories or do any other sanity checks, use with caution! + /// + public void ForceToSlot(Item item, int index) + { + slots[index].Add(item); + item.ParentInventory = this; + if (item.body != null) + { + item.body.Enabled = false; + item.body.BodyType = FarseerPhysics.BodyType.Dynamic; + } + } + + /// + /// Removes an item from a specific slot. Doesn't do any sanity checks, use with caution! + /// + public void ForceRemoveFromSlot(Item item, int index) + { + slots[index].RemoveItem(item); + } + + public void SharedWrite(IWriteMessage msg, object[] extraData = null) { msg.Write((byte)capacity); for (int i = 0; i < capacity; i++) { - msg.Write((ushort)(Items[i] == null ? 0 : Items[i].ID)); + msg.WriteRangedInteger(slots[i].ItemCount, 0, MaxStackSize); + foreach (Item item in slots[i].Items) + { + msg.Write((ushort)(item == null ? 0 : item.ID)); + } } } @@ -482,29 +862,17 @@ namespace Barotrauma { for (int i = 0; i < capacity; i++) { - if (Items[i] == null) continue; - foreach (ItemContainer itemContainer in Items[i].GetComponents()) + if (!slots[i].Any()) { continue; } + foreach (Item item in slots[i].Items) { - itemContainer.Inventory.DeleteAllItems(); + foreach (ItemContainer itemContainer in item.GetComponents()) + { + itemContainer.Inventory.DeleteAllItems(); + } } - Items[i].Remove(); + slots[i].Items.ForEachMod(it => it.Remove()); + slots[i].RemoveAllItems(); } } - - public List GetAllItems() - { - List deletedItems = new List(); - for (int i = 0; i < capacity; i++) - { - if (Items[i] == null) continue; - foreach (ItemContainer itemContainer in Items[i].GetComponents()) - { - deletedItems.AddRange(itemContainer.Inventory.GetAllItems()); - } - deletedItems.Add(Items[i]); - } - - return deletedItems; - } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index ec69d183f..d5a89bef0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -20,7 +20,7 @@ using Microsoft.Xna.Framework.Graphics; namespace Barotrauma { - partial class Item : MapEntity, IDamageable, ISerializableEntity, IServerSerializable, IClientSerializable + partial class Item : MapEntity, IDamageable, IIgnorable, ISerializableEntity, IServerSerializable, IClientSerializable { public static List ItemList = new List(); public ItemPrefab Prefab => prefab as ItemPrefab; @@ -76,7 +76,7 @@ namespace Barotrauma private readonly bool hasWaterStatusEffects; private Inventory parentInventory; - private readonly Inventory ownInventory; + private readonly ItemInventory ownInventory; private Rectangle defaultRect; @@ -171,6 +171,43 @@ namespace Barotrauma set; } + /// + /// Use to also check + /// + [Editable, Serialize(false, true, description: "When enabled, item is interactable only for characters on non-player teams.", alwaysUseInstanceValues: true)] + public bool NonPlayerTeamInteractable + { + get; + set; + } + + /// + /// Checks both and + /// + public bool IsPlayerTeamInteractable + { + get + { + return !NonInteractable && !NonPlayerTeamInteractable; + } + } + + /// + /// Returns interactibility based on whether the character is on a player team + /// + public bool IsInteractable(Character character) + { + if (character != null && character.IsOnPlayerTeam) + { + + return IsPlayerTeamInteractable; + } + else + { + return !NonInteractable; + } + } + private float rotationRad; [Editable(0.0f, 360.0f, DecimalCount = 1, ValueStep = 1f), Serialize(0.0f, true)] @@ -414,6 +451,7 @@ namespace Barotrauma if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } if (!MathUtils.IsValid(value)) { return; } if (Indestructible) { return; } + if (InvulnerableToDamage && value <= condition) { return;} float prev = condition; bool wasInFullCondition = IsFullCondition; @@ -466,10 +504,13 @@ namespace Barotrauma /// public bool Indestructible { - get { return indestructible ?? Prefab.Indestructible; } - set { indestructible = value; } + get => indestructible ?? Prefab.Indestructible; + set => indestructible = value; } + [Editable, Serialize(false, isSaveable: true, "When enabled will prevent the item from taking damage from all sources")] + public bool InvulnerableToDamage { get; set; } + public bool StolenDuringRound; private bool spawnedInOutpost; @@ -566,12 +607,12 @@ namespace Barotrauma } //which type of inventory slots (head, torso, any, etc) the item can be placed in - public List AllowedSlots + public IEnumerable AllowedSlots { get { Pickable p = GetComponent(); - return (p == null) ? new List() { InvSlotType.Any } : p.AllowedSlots; + return (p == null) ? InvSlotType.Any.ToEnumerable() : p.AllowedSlots; } } @@ -589,19 +630,11 @@ namespace Barotrauma { get { - // It's not a good practice to return null if the method tells that it returns a collection, because: - // a) the user has to handle this -> more code and more null reference exceptions - // b) it makes it more difficult to make use of chained function calls (which are quite powerful), although '?' makes it possible - // c) it's against the functional paradigm that e.g. Linq follows (for good reasons) - // In general, it's better to return an empty collection instead, - // but changing it here might cause unwanted implications. - // Also it can be a minor optimization to return null instead of creating an empty collection, - // but if that's the case I'd prefer caching an empty collection and using that instead. Just something to consider in the future. - return ownInventory?.Items.Where(i => i != null); + return ownInventory?.AllItems ?? Enumerable.Empty(); } } - public Inventory OwnInventory + public ItemInventory OwnInventory { get { return ownInventory; } } @@ -643,6 +676,8 @@ namespace Barotrauma get { return allPropertyObjects; } } + public bool IgnoreByAI => OrderedToBeIgnored || HasTag("ignorebyai"); + public bool OrderedToBeIgnored { get; set; } public Item(ItemPrefab itemPrefab, Vector2 position, Submarine submarine, ushort id = Entity.NullEntityID) : this(new Rectangle( @@ -845,6 +880,8 @@ namespace Barotrauma partial void InitProjSpecific(); + public bool IsContainerPreferred(ItemContainer container, out bool isPreferencesDefined, out bool isSecondary) => Prefab.IsContainerPreferred(this, container, out isPreferencesDefined, out isSecondary); + public override MapEntity Clone() { Item clone = new Item(rect, Prefab, Submarine, callOnItemLoaded: false) @@ -899,14 +936,12 @@ namespace Barotrauma component.OnItemLoaded(); } - if (ContainedItems != null) + foreach (Item containedItem in ContainedItems) { - foreach (Item containedItem in ContainedItems) - { - var containedClone = containedItem.Clone(); - clone.ownInventory.TryPutItem(containedClone as Item, null); - } - } + var containedClone = containedItem.Clone(); + clone.ownInventory.TryPutItem(containedClone as Item, null); + } + return clone; } @@ -1141,13 +1176,13 @@ namespace Barotrauma { if (parentInventory != null && parentInventory.Owner != null) { - if (parentInventory.Owner is Character) + if (parentInventory.Owner is Character character) { - CurrentHull = ((Character)parentInventory.Owner).AnimController.CurrentHull; + CurrentHull = character.AnimController.CurrentHull; } - else if (parentInventory.Owner is Item) + else if (parentInventory.Owner is Item item) { - CurrentHull = ((Item)parentInventory.Owner).CurrentHull; + CurrentHull = item.CurrentHull; } Submarine = parentInventory.Owner.Submarine; @@ -1157,7 +1192,7 @@ namespace Barotrauma } CurrentHull = Hull.FindHull(WorldPosition, CurrentHull); - if (body != null && body.Enabled) + if (body != null && body.Enabled && (body.BodyType == BodyType.Dynamic || Submarine == null)) { Submarine = CurrentHull?.Submarine; body.Submarine = Submarine; @@ -1180,7 +1215,7 @@ namespace Barotrauma } /// - /// Is the item or any of its containers of the item set to be ignored? + /// Should this item or any of its containers be ignored by the AI? /// public bool IsThisOrAnyContainerIgnoredByAI() { @@ -1308,22 +1343,17 @@ namespace Barotrauma if (effect.HasTargetType(StatusEffect.TargetType.Contained)) { - var containedItems = ownInventory?.Items; - if (containedItems != null) + foreach (Item containedItem in ContainedItems) { - foreach (Item containedItem in containedItems) + if (effect.TargetIdentifiers != null && + !effect.TargetIdentifiers.Contains(containedItem.prefab.Identifier) && + !effect.TargetIdentifiers.Any(id => containedItem.HasTag(id))) { - if (containedItem == null) { continue; } - if (effect.TargetIdentifiers != null && - !effect.TargetIdentifiers.Contains(containedItem.prefab.Identifier) && - !effect.TargetIdentifiers.Any(id => containedItem.HasTag(id))) - { - continue; - } - - hasTargets = true; - targets.Add(containedItem); + continue; } + + hasTargets = true; + targets.Add(containedItem); } } @@ -1385,7 +1415,7 @@ namespace Barotrauma public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = true) { - if (Indestructible) { return new AttackResult(); } + if (Indestructible || InvulnerableToDamage) { return new AttackResult(); } float damageAmount = attack.GetItemDamage(deltaTime); Condition -= damageAmount; @@ -1575,8 +1605,7 @@ namespace Barotrauma body.SetTransform(body.SimPosition + prevSub.SimPosition - Submarine.SimPosition, body.Rotation); } - var containedItems = ownInventory?.Items; - if (Submarine != prevSub && containedItems != null) + if (Submarine != prevSub) { foreach (Item containedItem in ContainedItems) { @@ -1664,15 +1693,10 @@ namespace Barotrauma #endif } - var containedItems = ownInventory?.Items; - if (containedItems != null) + foreach (Item contained in ContainedItems) { - foreach (Item contained in containedItems) - { - if (contained == null) { continue; } - if (contained.body != null) { contained.HandleCollision(impact); } - } - } + if (contained.body != null) { contained.HandleCollision(impact); } + } } } @@ -1936,7 +1960,7 @@ namespace Barotrauma Skill requiredSkill = null; float skillMultiplier = 1; #endif - if (NonInteractable) { return false; } + if (!IsInteractable(picker)) { return false; } foreach (ItemComponent ic in components) { bool pickHit = false, selectHit = false; @@ -2040,24 +2064,19 @@ namespace Barotrauma public float GetContainedItemConditionPercentage() { - var containedItems = ContainedItems; + if (ownInventory == null) { return -1; } - if (containedItems != null) + float condition = 0f; + float maxCondition = 0f; + foreach (Item item in ContainedItems) { - float condition = 0f; - float maxCondition = 0f; - - foreach (Item item in containedItems) - { - condition += item.condition; - maxCondition += item.MaxCondition; - } - - if (maxCondition > 0.0f) - { - return condition / maxCondition; - } + condition += item.condition; + maxCondition += item.MaxCondition; } + if (maxCondition > 0.0f) + { + return condition / maxCondition; + } return -1; } @@ -2252,7 +2271,6 @@ namespace Barotrauma public void Unequip(Character character) { - character.DeselectItem(this); foreach (ItemComponent ic in components) { ic.Unequip(character); } } @@ -2689,11 +2707,18 @@ namespace Barotrauma public virtual void Reset() { + var holdable = GetComponent(); + bool wasAttached = holdable?.Attached ?? false; + SerializableProperties = SerializableProperty.DeserializeProperties(this, Prefab.ConfigElement); Sprite.ReloadXML(); SpriteDepth = Sprite.Depth; condition = MaxCondition; components.ForEach(c => c.Reset()); + if (wasAttached) + { + holdable.AttachToWall(); + } } public override void OnMapLoaded() @@ -2740,11 +2765,7 @@ namespace Barotrauma foreach (Character character in Character.CharacterList) { - if (character.SelectedConstruction == this) character.SelectedConstruction = null; - for (int i = 0; i < character.SelectedItems.Length; i++) - { - if (character.SelectedItems[i] == this) character.SelectedItems[i] = null; - } + if (character.SelectedConstruction == this) { character.SelectedConstruction = null; } } Door door = GetComponent(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs index 35a35d89d..e2e783677 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs @@ -3,6 +3,7 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Linq; namespace Barotrauma { @@ -22,19 +23,21 @@ namespace Barotrauma public override int FindAllowedSlot(Item item) { - if (ItemOwnsSelf(item)) return -1; + if (ItemOwnsSelf(item)) { return -1; } + //item is already in the inventory! + if (Contains(item)) { return -1; } + if (!container.CanBeContained(item)) { return -1; } + + //try to stack first for (int i = 0; i < capacity; i++) { - //item is already in the inventory! - if (Items[i] == item) return -1; + if (slots[i].Any() && CanBePut(item, i)) { return i; } } - if (!container.CanBeContained(item)) return -1; - for (int i = 0; i < capacity; i++) { - if (Items[i] == null) return i; + if (CanBePut(item, i)) { return i; } } return -1; @@ -42,12 +45,50 @@ namespace Barotrauma public override bool CanBePut(Item item, int i) { - if (ItemOwnsSelf(item)) return false; - if (i < 0 || i >= Items.Length) return false; - return (item != null && Items[i] == null && container.CanBeContained(item)); + if (ItemOwnsSelf(item)) { return false; } + if (i < 0 || i >= slots.Length) { return false; } + if (!container.CanBeContained(item)) { return false; } + return item != null && slots[i].CanBePut(item) && slots[i].ItemCount < container.MaxStackSize; } - public override bool TryPutItem(Item item, Character user, List allowedSlots = null, bool createNetworkEvent = true) + public override bool CanBePut(ItemPrefab itemPrefab, int i) + { + if (i < 0 || i >= slots.Length) { return false; } + if (!container.CanBeContained(itemPrefab)) { return false; } + return itemPrefab != null && slots[i].CanBePut(itemPrefab) && slots[i].ItemCount < container.MaxStackSize; + } + + public override int HowManyCanBePut(ItemPrefab itemPrefab, int i) + { + if (itemPrefab == null) { return 0; } + if (i < 0 || i >= slots.Length) { return 0; } + if (!container.CanBeContained(itemPrefab)) { return 0; } + return slots[i].HowManyCanBePut(itemPrefab, maxStackSize: Math.Min(itemPrefab.MaxStackSize, container.MaxStackSize)); + } + + public override bool IsFull(bool takeStacksIntoAccount = false) + { + if (takeStacksIntoAccount) + { + for (int i = 0; i < capacity; i++) + { + if (!slots[i].Any()) { return false; } + var item = slots[i].FirstOrDefault(); + if (slots[i].ItemCount < Math.Min(item.Prefab.MaxStackSize, container.MaxStackSize)) { return false; } + } + } + else + { + for (int i = 0; i < capacity; i++) + { + if (!slots[i].Any()) { return false; } + } + } + + return true; + } + + public override bool TryPutItem(Item item, Character user, IEnumerable allowedSlots = null, bool createNetworkEvent = true) { bool wasPut = base.TryPutItem(item, user, allowedSlots, createNetworkEvent); @@ -55,8 +96,7 @@ namespace Barotrauma { foreach (Character c in Character.CharacterList) { - if (!c.HasSelectedItem(item)) continue; - + if (!c.HeldItems.Contains(item)) { continue; } item.Unequip(c); break; } @@ -71,13 +111,11 @@ namespace Barotrauma public override bool TryPutItem(Item item, int i, bool allowSwapping, bool allowCombine, Character user, bool createNetworkEvent = true) { bool wasPut = base.TryPutItem(item, i, allowSwapping, allowCombine, user, createNetworkEvent); - if (wasPut) { foreach (Character c in Character.CharacterList) { - if (!c.HasSelectedItem(item)) continue; - + if (!c.HeldItems.Contains(item)) { continue; } item.Unequip(c); break; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index 8ddbd9ed0..a1c97954a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -22,6 +22,7 @@ namespace Barotrauma public readonly float OutCondition; //should the condition of the deconstructed item be copied to the output items public readonly bool CopyCondition; + public float Commonness { get; } public DeconstructItem(XElement element) { @@ -30,6 +31,7 @@ namespace Barotrauma MaxCondition = element.GetAttributeFloat("maxcondition", 1.0f); OutCondition = element.GetAttributeFloat("outcondition", 1.0f); CopyCondition = element.GetAttributeBool("copycondition", false); + Commonness = element.GetAttributeFloat("commonness", 1.0f); } } @@ -66,6 +68,7 @@ namespace Barotrauma public readonly float RequiredTime; public readonly float OutCondition; //Percentage-based from 0 to 1 public readonly List RequiredSkills; + public int Amount { get; } public FabricationRecipe(XElement element, ItemPrefab itemPrefab) { @@ -79,6 +82,7 @@ namespace Barotrauma RequiredTime = element.GetAttributeFloat("requiredtime", 1.0f); OutCondition = element.GetAttributeFloat("outcondition", 1.0f); RequiredItems = new List(); + Amount = element.GetAttributeInt("amount", 1); foreach (XElement subElement in element.Elements()) { @@ -118,7 +122,7 @@ namespace Barotrauma continue; } - var existing = RequiredItems.Find(r => r.ItemPrefabs.Count == 1 && r.ItemPrefabs[0] == requiredItem); + var existing = RequiredItems.Find(r => r.ItemPrefabs.Count == 1 && r.ItemPrefabs[0] == requiredItem && MathUtils.NearlyEqual(r.MinCondition, minCondition)); if (existing == null) { RequiredItems.Add(new RequiredItem(requiredItem, count, minCondition, useCondition)); @@ -137,7 +141,7 @@ namespace Barotrauma continue; } - var existing = RequiredItems.Find(r => r.ItemPrefabs.SequenceEqual(matchingItems)); + var existing = RequiredItems.Find(r => r.ItemPrefabs.SequenceEqual(matchingItems) && MathUtils.NearlyEqual(r.MinCondition, minCondition)); if (existing == null) { RequiredItems.Add(new RequiredItem(matchingItems, count, minCondition, useCondition)); @@ -157,7 +161,10 @@ namespace Barotrauma { public readonly HashSet Primary = new HashSet(); public readonly HashSet Secondary = new HashSet(); + public float SpawnProbability { get; private set; } + public float MaxCondition { get; private set; } + public float MinCondition { get; private set; } public int MinAmount { get; private set; } public int MaxAmount { get; private set; } @@ -168,6 +175,8 @@ namespace Barotrauma SpawnProbability = element.GetAttributeFloat("spawnprobability", 0.0f); MinAmount = element.GetAttributeInt("minamount", 0); MaxAmount = Math.Max(MinAmount, element.GetAttributeInt("maxamount", 0)); + MaxCondition = element.GetAttributeFloat("maxcondition", 100f); + MinCondition = element.GetAttributeFloat("mincondition", 0f); if (element.Attribute("spawnprobability") == null) { @@ -498,10 +507,27 @@ namespace Barotrauma public bool CanSpriteFlipY { get; private set; } + private int maxStackSize; + [Serialize(1, false)] + public int MaxStackSize + { + get { return maxStackSize; } + set { maxStackSize = MathHelper.Clamp(value, 1, Inventory.MaxStackSize); } + } + public Vector2 Size => size; public bool CanBeBought => (defaultPrice != null && defaultPrice.CanBeBought) || (locationPrices != null && locationPrices.Any(p => p.Value.CanBeBought)); + /// + /// Any item with a Price element in the definition can be sold everywhere. + /// + public bool CanBeSold => defaultPrice != null; + + public bool RandomDeconstructionOutput { get; } + + public int RandomDeconstructionOutputAmount { get; } + public static void RemoveByFile(string filePath) => Prefabs.RemoveByFile(filePath); public static void LoadFromFile(ContentFile file) @@ -876,6 +902,8 @@ namespace Barotrauma case "deconstruct": DeconstructTime = subElement.GetAttributeFloat("time", 1.0f); AllowDeconstruct = true; + RandomDeconstructionOutput = subElement.GetAttributeBool("chooserandom", false); + RandomDeconstructionOutputAmount = subElement.GetAttributeInt("amount", 1); foreach (XElement deconstructItem in subElement.Elements()) { if (deconstructItem.Attribute("name") != null) @@ -883,10 +911,9 @@ namespace Barotrauma DebugConsole.ThrowError("Error in item config \"" + Name + "\" - use item identifiers instead of names to configure the deconstruct items."); continue; } - DeconstructItems.Add(new DeconstructItem(deconstructItem)); } - + RandomDeconstructionOutputAmount = Math.Min(RandomDeconstructionOutputAmount, DeconstructItems.Count); break; case "fabricate": case "fabricable": @@ -1073,32 +1100,34 @@ namespace Barotrauma } } - public bool IsContainerPreferred(ItemContainer itemContainer, out bool isPreferencesDefined, out bool isSecondary) + public bool IsContainerPreferred(Item item, ItemContainer targetContainer, out bool isPreferencesDefined, out bool isSecondary) { isPreferencesDefined = PreferredContainers.Any(); isSecondary = false; if (!isPreferencesDefined) { return true; } - if (PreferredContainers.Any(pc => IsContainerPreferred(pc.Primary, itemContainer))) + if (PreferredContainers.Any(pc => IsItemConditionAcceptable(item, pc) && IsContainerPreferred(pc.Primary, targetContainer))) { return true; } isSecondary = true; - return PreferredContainers.Any(pc => IsContainerPreferred(pc.Secondary, itemContainer)); + return PreferredContainers.Any(pc => IsItemConditionAcceptable(item, pc) && IsContainerPreferred(pc.Secondary, targetContainer)); } - public bool IsContainerPreferred(string[] identifiersOrTags, out bool isPreferencesDefined, out bool isSecondary) + public bool IsContainerPreferred(Item item, string[] identifiersOrTags, out bool isPreferencesDefined, out bool isSecondary) { isPreferencesDefined = PreferredContainers.Any(); isSecondary = false; if (!isPreferencesDefined) { return true; } - if (PreferredContainers.Any(pc => IsContainerPreferred(pc.Primary, identifiersOrTags))) + if (PreferredContainers.Any(pc => IsItemConditionAcceptable(item, pc) && IsContainerPreferred(pc.Primary, identifiersOrTags))) { return true; } isSecondary = true; - return PreferredContainers.Any(pc => IsContainerPreferred(pc.Secondary, identifiersOrTags)); + return PreferredContainers.Any(pc => IsItemConditionAcceptable(item, pc) && IsContainerPreferred(pc.Secondary, identifiersOrTags)); } + private bool IsItemConditionAcceptable(Item item, PreferredContainer pc) => item.ConditionPercentage >= pc.MinCondition && item.ConditionPercentage <= pc.MaxCondition; + public static bool IsContainerPreferred(IEnumerable preferences, ItemContainer c) => preferences.Any(id => c.Item.Prefab.Identifier == id || c.Item.HasTag(id)); public static bool IsContainerPreferred(IEnumerable preferences, IEnumerable ids) => ids.Any(id => preferences.Contains(id)); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs index edf4bc59a..905b91ff3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs @@ -17,6 +17,7 @@ namespace Barotrauma } public bool IsOptional { get; set; } + public bool MatchOnEmpty { get; set; } public bool IgnoreInEditor { get; set; } @@ -30,6 +31,10 @@ namespace Barotrauma public string Msg; public string MsgTag; + /// + /// Should broken (0 condition) items be excluded + /// + public bool ExcludeBroken { get; private set; } public RelationType Type { @@ -107,21 +112,20 @@ namespace Barotrauma return CheckContained(parentItem); case RelationType.Container: if (parentItem == null || parentItem.Container == null) { return MatchOnEmpty; } - return parentItem.Container.Condition > 0.0f && MatchesItem(parentItem.Container); + return (!ExcludeBroken || parentItem.Container.Condition > 0.0f) && MatchesItem(parentItem.Container); case RelationType.Equipped: if (character == null) { return false; } - if (MatchOnEmpty && character.SelectedItems.All(it => it == null)) { return true; } - foreach (Item equippedItem in character.SelectedItems) + if (MatchOnEmpty && !character.HeldItems.Any()) { return true; } + foreach (Item equippedItem in character.HeldItems) { if (equippedItem == null) { continue; } - if (equippedItem.Condition > 0.0f && MatchesItem(equippedItem)) { return true; } + if ((!ExcludeBroken || equippedItem.Condition > 0.0f) && MatchesItem(equippedItem)) { return true; } } break; case RelationType.Picked: if (character == null || character.Inventory == null) { return false; } - foreach (Item pickedItem in character.Inventory.Items) + foreach (Item pickedItem in character.Inventory.AllItems) { - if (pickedItem == null) { continue; } if (MatchesItem(pickedItem)) { return true; } } break; @@ -134,18 +138,16 @@ namespace Barotrauma private bool CheckContained(Item parentItem) { - var containedItems = parentItem.OwnInventory?.Items; - if (containedItems == null) { return false; } + if (parentItem.OwnInventory == null) { return false; } - if (MatchOnEmpty && !containedItems.Any(ci => ci != null)) + if (MatchOnEmpty && parentItem.OwnInventory.IsEmpty()) { return true; } - foreach (Item contained in containedItems) + foreach (Item contained in parentItem.ContainedItems) { - if (contained == null) { continue; } - if (contained.Condition > 0.0f && MatchesItem(contained)) { return true; } + if ((!ExcludeBroken || contained.Condition > 0.0f) && MatchesItem(contained)) { return true; } if (CheckContained(contained)) { return true; } } return false; @@ -157,7 +159,8 @@ namespace Barotrauma new XAttribute("items", JoinedIdentifiers), new XAttribute("type", type.ToString()), new XAttribute("optional", IsOptional), - new XAttribute("ignoreineditor", IgnoreInEditor)); + new XAttribute("ignoreineditor", IgnoreInEditor), + new XAttribute("excludebroken", ExcludeBroken)); if (excludedIdentifiers.Length > 0) { @@ -215,9 +218,13 @@ namespace Barotrauma } } + if (identifiers.Length == 0 && excludedIdentifiers.Length == 0 && !returnEmpty) { return null; } - RelatedItem ri = new RelatedItem(identifiers, excludedIdentifiers); + RelatedItem ri = new RelatedItem(identifiers, excludedIdentifiers) + { + ExcludeBroken = element.GetAttributeBool("excludebroken", true) + }; string typeStr = element.GetAttributeString("type", ""); if (string.IsNullOrEmpty(typeStr)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs index 41c935e8b..ffead791e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs @@ -91,6 +91,9 @@ namespace Barotrauma.MapCreatures.Behavior public List> debugSearchLines = new List>(); #endif + private static List _entityList = new List(); + public static IEnumerable EntityList => _entityList; + public enum NetworkHeader { Spawn, @@ -199,6 +202,9 @@ namespace Barotrauma.MapCreatures.Behavior [Serialize(5f, true, "How much damage is taken from open fires")] public float FireVulnerability { get; set; } + [Serialize(0.5f, true, "How much resistance against fire is gained while submerged.")] + public float SubmergedWaterResistance { get; set; } + [Serialize(0.8f, true, "What depth the branches will be drawn on")] public float BranchDepth { get; set; } @@ -295,6 +301,7 @@ namespace Barotrauma.MapCreatures.Behavior LoadPrefab(prefab.Element); StateMachine = new BallastFloraStateMachine(this); if (firstGrowth) { GenerateStem(); } + _entityList.Add(this); } partial void LoadPrefab(XElement element); @@ -427,7 +434,8 @@ namespace Barotrauma.MapCreatures.Behavior if (GameMain.DebugDraw) { - GUI.AddMessage($"{(int)branch.AccumulatedDamage}", GUI.Style.Red, GetWorldPosition() + branch.Position, Vector2.UnitY * 10.0f, 3f, playSound: false); + var pos = (Parent?.Position ?? Vector2.Zero) + Offset + branch.Position; + GUI.AddMessage($"{(int)branch.AccumulatedDamage}", GUI.Style.Red, pos, Vector2.UnitY * 10.0f, 3f, playSound: false, subId: Parent?.Submarine?.ID ?? -1); } #elif SERVER SendNetworkMessage(this, NetworkHeader.BranchDamage, branch, branch.AccumulatedDamage); @@ -469,7 +477,7 @@ namespace Barotrauma.MapCreatures.Behavior } } } - + UpdateSelfDamage(deltaTime); if (Anger > 1f) @@ -862,6 +870,7 @@ namespace Barotrauma.MapCreatures.Behavior public void DamageBranch(BallastFloraBranch branch, float amount, AttackType type, Character? attacker = null) { + float damage = amount; // damage is handled server side currently if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } @@ -875,7 +884,7 @@ namespace Barotrauma.MapCreatures.Behavior { if (IsInWater(branch)) { - return; + damage *= 1f - SubmergedWaterResistance; } if (defenseCooldown <= 0) @@ -883,24 +892,24 @@ namespace Barotrauma.MapCreatures.Behavior if (!(StateMachine.State is DefendWithPumpState)) { StateMachine.EnterState(new DefendWithPumpState(branch, ClaimedTargets, attacker)); - defenseCooldown = 60f; + defenseCooldown = 180f; } defenseCooldown = 10f; } } - branch.AccumulatedDamage += amount; + branch.AccumulatedDamage += damage; - branch.Health -= amount; + branch.Health -= damage; if (type != AttackType.Other) { - Anger += amount * 0.001f; + Anger += damage * 0.001f; } #if SERVER - GameMain.Server?.KarmaManager?.OnBallastFloraDamaged(attacker, amount); + GameMain.Server?.KarmaManager?.OnBallastFloraDamaged(attacker, damage); #endif if (branch.Health < 0) @@ -923,6 +932,8 @@ namespace Barotrauma.MapCreatures.Behavior { target.Infector = null; } + + _entityList.Remove(this); } public void RemoveBranch(BallastFloraBranch branch) @@ -1028,6 +1039,8 @@ namespace Barotrauma.MapCreatures.Behavior target.Infector = null; } + StateMachine?.State?.Exit(); + // clean up leftover (can probably be removed) foreach (Body body in bodies) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs index b08738e22..b09272cc1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs @@ -12,8 +12,9 @@ namespace Barotrauma public const ushort NullEntityID = 0; public const ushort EntitySpawnerID = ushort.MaxValue; public const ushort RespawnManagerID = ushort.MaxValue - 1; + public const ushort DummyID = ushort.MaxValue - 2; - public const ushort ReservedIDStart = ushort.MaxValue - 2; + public const ushort ReservedIDStart = ushort.MaxValue - 3; private static Dictionary dictionary = new Dictionary(); public static IEnumerable GetEntities() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index 10ae9497f..187f354f3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -25,6 +25,7 @@ namespace Barotrauma private readonly float screenColorRange, screenColorDuration; private bool sparks, shockwave, flames, smoke, flash, underwaterBubble; + private bool playTinnitus; private bool applyFireEffects; private readonly float flashDuration; private readonly float? flashRange; @@ -63,6 +64,8 @@ namespace Barotrauma underwaterBubble = element.GetAttributeBool("underwaterbubble", true); smoke = element.GetAttributeBool("smoke", true); + playTinnitus = element.GetAttributeBool("playtinnitus", true); + applyFireEffects = element.GetAttributeBool("applyfireeffects", flames); flash = element.GetAttributeBool("flash", true); @@ -225,7 +228,7 @@ namespace Barotrauma partial void ExplodeProjSpecific(Vector2 worldPosition, Hull hull); - public static void DamageCharacters(Vector2 worldPosition, Attack attack, float force, Entity damageSource, Character attacker) + private void DamageCharacters(Vector2 worldPosition, Attack attack, float force, Entity damageSource, Character attacker) { if (attack.Range <= 0.0f) { return; } @@ -323,10 +326,10 @@ namespace Barotrauma } } - if (c == Character.Controlled && !c.IsDead) + if (c == Character.Controlled && !c.IsDead && playTinnitus) { Limb head = c.AnimController.GetLimb(LimbType.Head); - if (damages.TryGetValue(head, out float headDamage) && headDamage > 0.0f && distFactors.TryGetValue(head, out float headFactor)) + if (head != null && damages.TryGetValue(head, out float headDamage) && headDamage > 0.0f && distFactors.TryGetValue(head, out float headFactor)) { PlayTinnitusProjSpecific(headFactor); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs index f8a8b23a3..cb846612f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs @@ -299,7 +299,7 @@ namespace Barotrauma //GetApproximateDistance returns float.MaxValue if there's no path through open gaps between the hulls (e.g. if there's a door/wall in between) if (hull.GetApproximateDistance(Position, c.Position, c.CurrentHull, 10000.0f) > size.X + DamageRange) { - return; + continue; } float dmg = (float)Math.Sqrt(Math.Min(500, size.X)) * deltaTime / c.AnimController.Limbs.Count(l => !l.IsSevered && !l.Hidden); @@ -346,11 +346,17 @@ namespace Barotrauma //don't apply OnFire effects if the item is inside a fireproof container //(or if it's inside a container that's inside a fireproof container, etc) Item container = item.Container; + bool fireProof = false; while (container != null) { - if (container.FireProof) return; + if (container.FireProof) + { + fireProof = true; + break; + } container = container.Container; } + if (fireProof) { continue; } float range = (float)Math.Sqrt(size.X) * 10.0f; if (item.Position.X < position.X - range || item.Position.X > position.X + size.X + range) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs index 950041ad6..8da12066f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs @@ -1,11 +1,9 @@ using Barotrauma.Items.Components; -using Barotrauma.Networking; using FarseerPhysics; using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Xml.Linq; namespace Barotrauma @@ -120,10 +118,17 @@ namespace Barotrauma return "Gap"; } } - + public Gap(MapEntityPrefab prefab, Rectangle rectangle) - : this (rectangle, Submarine.MainSub) - { } + : this(rectangle, Submarine.MainSub) + { +#if CLIENT + if (SubEditorScreen.IsSubEditor()) + { + SubEditorScreen.StoreCommand(new AddOrDeleteCommand(new List { this }, false)); + } +#endif + } public Gap(Rectangle rect, Submarine submarine) : this(rect, rect.Width < rect.Height, submarine) @@ -233,6 +238,13 @@ namespace Barotrauma { Hull[] hulls = new Hull[2]; + foreach (var linked in linkedTo) + { + if (linked is Hull hull) + { + hull.ConnectedGaps.Remove(this); + } + } linkedTo.Clear(); Vector2[] searchPos = new Vector2[2]; @@ -595,7 +607,7 @@ namespace Barotrauma } Vector2 rayStart = ConvertUnits.ToSimUnits(WorldPosition); - Vector2 rayEnd = rayStart + rayDir * 500.0f; + Vector2 rayEnd = rayStart + rayDir * 5.0f; var levelCells = Level.Loaded.GetCells(WorldPosition, searchDepth: 1); foreach (var cell in levelCells) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index c069066ae..08a145013 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -397,7 +397,12 @@ namespace Barotrauma public Hull(MapEntityPrefab prefab, Rectangle rectangle) : this (prefab, rectangle, Submarine.MainSub) { - +#if CLIENT + if (SubEditorScreen.IsSubEditor()) + { + SubEditorScreen.StoreCommand(new AddOrDeleteCommand(new List { this }, false)); + } +#endif } public Hull(MapEntityPrefab prefab, Rectangle rectangle, Submarine submarine, ushort id = Entity.NullEntityID) @@ -791,7 +796,7 @@ namespace Barotrauma //make waves propagate through horizontal gaps foreach (Gap gap in ConnectedGaps) { - if (this != gap.linkedTo[0] as Hull) + if (this != gap.linkedTo.FirstOrDefault() as Hull) { //let the first linked hull handle the water propagation continue; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/ISpatialEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/ISpatialEntity.cs index 88d8bb92f..5f1b94581 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/ISpatialEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/ISpatialEntity.cs @@ -8,6 +8,11 @@ namespace Barotrauma Vector2 WorldPosition { get; } Vector2 SimPosition { get; } Submarine Submarine { get; } - bool IgnoreByAI => false; + } + + interface IIgnorable : ISpatialEntity + { + bool IgnoreByAI { get; } + bool OrderedToBeIgnored { get; set; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs index 8037cdbe6..2ed64bd3d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs @@ -65,8 +65,21 @@ namespace Barotrauma var containerElement = entityElement.Elements().FirstOrDefault(e => e.Name.LocalName.Equals("itemcontainer", StringComparison.OrdinalIgnoreCase)); if (containerElement == null) { continue; } - var itemIds = containerElement.GetAttributeIntArray("contained", new int[0]); - containedItemIDs.AddRange(itemIds.Select(id => (ushort)id)); + string containedString = containerElement.GetAttributeString("contained", ""); + string[] itemIdStrings = containedString.Split(','); + var itemIds = new List[itemIdStrings.Length]; + for (int i = 0; i < itemIdStrings.Length; i++) + { + itemIds[i] ??= new List(); + foreach (string idStr in itemIdStrings[i].Split(';')) + { + if (int.TryParse(idStr, out int id)) + { + itemIds[i].Add((ushort)id); + containedItemIDs.Add((ushort)id); + } + } + } } int minX = int.MaxValue, minY = int.MaxValue; @@ -110,16 +123,18 @@ namespace Barotrauma protected override void CreateInstance(Rectangle rect) { - var loaded = CreateInstance(rect.Location.ToVector2(), Submarine.MainSub); #if CLIENT + var loaded = CreateInstance(rect.Location.ToVector2(), Submarine.MainSub, selectInstance: Screen.Selected == GameMain.SubEditorScreen); if (Screen.Selected is SubEditorScreen) { SubEditorScreen.StoreCommand(new AddOrDeleteCommand(loaded, false, handleInventoryBehavior: false)); } +#else + var loaded = CreateInstance(rect.Location.ToVector2(), Submarine.MainSub); #endif } - public List CreateInstance(Vector2 position, Submarine sub, bool selectPrefabs = false) + public List CreateInstance(Vector2 position, Submarine sub, bool selectInstance = false) { int idOffset = Entity.FindFreeID(1); if (MapEntity.mapEntityList.Any()) { idOffset = MapEntity.mapEntityList.Max(e => e.ID); } @@ -148,7 +163,7 @@ namespace Barotrauma MapEntity.MapLoaded(entities, true); #if CLIENT - if (Screen.Selected == GameMain.SubEditorScreen && selectPrefabs) + if (Screen.Selected == GameMain.SubEditorScreen && selectInstance) { MapEntity.SelectedList.Clear(); entities.ForEach(MapEntity.AddSelection); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerationParams.cs index 2bf8a1b1b..6bb62d0ab 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerationParams.cs @@ -1,6 +1,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Xml.Linq; @@ -135,6 +136,13 @@ namespace Barotrauma case "walledge": WallEdgeSprite = new Sprite(subElement); break; + case "overridecommonness": + string levelType = subElement.GetAttributeString("leveltype", "").ToLowerInvariant(); + if (!OverrideCommonness.ContainsKey(levelType)) + { + OverrideCommonness.Add(levelType, subElement.GetAttributeFloat("commonness", 1.0f)); + } + break; } } } @@ -193,5 +201,30 @@ namespace Barotrauma } } } + + public void Save(XElement element) + { + SerializableProperty.SerializeProperties(this, element, true); + foreach (KeyValuePair overrideCommonness in OverrideCommonness) + { + bool elementFound = false; + foreach (XElement subElement in element.Elements()) + { + if (subElement.Name.ToString().Equals("overridecommonness", StringComparison.OrdinalIgnoreCase) + && subElement.GetAttributeString("leveltype", "").Equals(overrideCommonness.Key, StringComparison.OrdinalIgnoreCase)) + { + subElement.Attribute("commonness").Value = overrideCommonness.Value.ToString("G", CultureInfo.InvariantCulture); + elementFound = true; + break; + } + } + if (!elementFound) + { + element.Add(new XElement("overridecommonness", + new XAttribute("leveltype", overrideCommonness.Key), + new XAttribute("commonness", overrideCommonness.Value.ToString("G", CultureInfo.InvariantCulture)))); + } + } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs index 03ce1a9b4..21ccdffd0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs @@ -169,14 +169,16 @@ namespace Barotrauma sw2.Start(); List pathCells = new List(); - + + if (targetCells.Count == 0) { return pathCells; } + VoronoiCell currentCell = targetCells[0]; currentCell.CellType = CellType.Path; pathCells.Add(currentCell); int currentTargetIndex = 0; - int iterationsLeft = cells.Count; + int iterationsLeft = cells.Count / 2; do { @@ -189,10 +191,14 @@ namespace Barotrauma if (adjacentCell == null) { continue; } double dist = MathUtils.Distance(adjacentCell.Site.Coord.X, adjacentCell.Site.Coord.Y, targetCells[currentTargetIndex].Site.Coord.X, targetCells[currentTargetIndex].Site.Coord.Y); dist += MathUtils.Distance(adjacentCell.Site.Coord.X, adjacentCell.Site.Coord.Y, currentCell.Site.Coord.X, currentCell.Site.Coord.Y) * 0.5f; - //disfavor small edges to prevent generating a very small passage - if (Vector2.Distance(currentCell.Edges[i].Point1, currentCell.Edges[i].Point2) < 200.0f) + + //disfavor short edges to prevent generating a very small passage + if (Vector2.DistanceSquared(currentCell.Edges[i].Point1, currentCell.Edges[i].Point2) < 150.0f * 150.0f) { - dist += 1000000; + //divide by the number of times the current cell has been used + // prevents the path from getting "stuck" (jumping back and forth between adjacent cells) + // if there's no other way to the destination than going through a short edge + dist *= 10.0f / Math.Max(pathCells.Count(c => c == currentCell), 1.0f); } if (dist < smallestDist) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/DestructibleLevelWall.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/DestructibleLevelWall.cs index 8c648d559..a9fb2d699 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/DestructibleLevelWall.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/DestructibleLevelWall.cs @@ -58,7 +58,7 @@ namespace Barotrauma public DestructibleLevelWall(List vertices, Color color, Level level, float? health = null, bool giftWrap = false) : base (vertices, color, level, giftWrap) { - MaxHealth = health ?? MathHelper.Clamp(Body.Mass, 100.0f, 1000.0f); + MaxHealth = health ?? MathHelper.Clamp(Body.Mass * 0.5f, 50.0f, 1000.0f); Cells.ForEach(c => c.IsDestructible = true); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 24c843ff0..a43c81020 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -45,14 +45,35 @@ namespace Barotrauma public bool IsValid; public Submarine Submarine; public Ruin Ruin; + public Cave Cave; - public InterestingPosition(Point position, PositionType positionType, bool isValid = true, Submarine submarine = null, Ruin ruin = null) + public InterestingPosition(Point position, PositionType positionType, Submarine submarine = null, bool isValid = true) { Position = position; PositionType = positionType; IsValid = isValid; Submarine = submarine; + Ruin = null; + Cave = null; + } + + public InterestingPosition(Point position, PositionType positionType, Ruin ruin, bool isValid = true) + { + Position = position; + PositionType = positionType; + IsValid = isValid; + Submarine = null; Ruin = ruin; + Cave = null; + } + public InterestingPosition(Point position, PositionType positionType, Cave cave, bool isValid = true) + { + Position = position; + PositionType = positionType; + IsValid = isValid; + Submarine = null; + Ruin = null; + Cave = cave; } } @@ -106,6 +127,8 @@ namespace Barotrauma public Point StartPos, EndPos; + public bool DisplayOnSonar; + public readonly CaveGenerationParams CaveGenerationParams; public Cave(CaveGenerationParams caveGenerationParams, Rectangle area, Point startPos, Point endPos) @@ -375,6 +398,7 @@ namespace Barotrauma minWidth = Math.Min(minWidth, MaxSubmarineWidth); } minWidth = Math.Min(minWidth, borders.Width / 5); + LevelData.MinMainPathWidth = minWidth; Rectangle pathBorders = borders; pathBorders.Inflate( @@ -435,6 +459,7 @@ namespace Barotrauma Point siteVariance = GenerationParams.VoronoiSiteVariance; siteCoordsX = new List((borders.Height / siteInterval.Y) * (borders.Width / siteInterval.Y)); siteCoordsY = new List((borders.Height / siteInterval.Y) * (borders.Width / siteInterval.Y)); + int caveSiteInterval = 500; for (int x = siteInterval.X / 2; x < borders.Width; x += siteInterval.X) { for (int y = siteInterval.Y / 2; y < borders.Height; y += siteInterval.Y) @@ -448,7 +473,7 @@ namespace Barotrauma { for (int i = 1; i < tunnel.Nodes.Count; i++) { - float minDist = Math.Max(tunnel.MinWidth, Math.Max(siteInterval.X, siteInterval.Y)) * 2.0f; + float minDist = Math.Max(tunnel.MinWidth * 2.0f, Math.Max(siteInterval.X, siteInterval.Y)); if (siteX < Math.Min(tunnel.Nodes[i - 1].X, tunnel.Nodes[i].X) - minDist) { continue; } if (siteX > Math.Max(tunnel.Nodes[i - 1].X, tunnel.Nodes[i].X) + minDist) { continue; } if (siteY < Math.Min(tunnel.Nodes[i - 1].Y, tunnel.Nodes[i].Y) - minDist) { continue; } @@ -459,7 +484,7 @@ namespace Barotrauma { closeToTunnel = true; tunnelDistSqr = MathUtils.LineSegmentToPointDistanceSquared(tunnel.Nodes[i - 1], tunnel.Nodes[i], new Point(siteX, siteY)); - if (tunnel.Type == TunnelType.Cave ) + if (tunnel.Type == TunnelType.Cave) { closeToCave = true; } @@ -474,31 +499,64 @@ namespace Barotrauma if (Rand.Range(0, 10, Rand.RandSync.Server) != 0) { continue; } } - if (closeToCave) - { - //add some more sites around caves to generate more small voronoi cells - if (x < borders.Width - siteInterval.X) - { - siteCoordsX.Add(x + Rand.Range(siteInterval.X / 4, siteInterval.X / 2, Rand.RandSync.Server)); - siteCoordsY.Add(y); - } - if (y < borders.Height - siteInterval.Y) - { - siteCoordsX.Add(x); - siteCoordsY.Add(y + Rand.Range(siteInterval.Y / 4, siteInterval.Y / 2, Rand.RandSync.Server)); - } - if (x < borders.Width - siteInterval.X && y < borders.Height - siteInterval.Y) - { - siteCoordsX.Add(x + Rand.Range(siteInterval.X / 4, siteInterval.X / 2, Rand.RandSync.Server)); - siteCoordsY.Add(y + Rand.Range(siteInterval.Y / 4, siteInterval.Y / 2, Rand.RandSync.Server)); - } - } - siteCoordsX.Add(siteX); siteCoordsY.Add(siteY); + + if (closeToCave) + { + for (int x2 = x; x2 < x + siteInterval.X; x2 += caveSiteInterval) + { + for (int y2 = y; y2 < y + siteInterval.Y; y2 += caveSiteInterval) + { + int caveSiteX = x2 + Rand.Int(caveSiteInterval / 2, Rand.RandSync.Server); + int caveSiteY = y2 + Rand.Int(caveSiteInterval / 2, Rand.RandSync.Server); + + bool tooClose = false; + for (int i = 0; i < siteCoordsX.Count; i++) + { + if (MathUtils.DistanceSquared(caveSiteX, caveSiteY, siteCoordsX[i], siteCoordsY[i]) < 10.0f * 10.0f) + { + tooClose = true; + break; + } + } + if (tooClose) { continue; } + siteCoordsX.Add(caveSiteX); + siteCoordsY.Add(caveSiteY); + } + } + + } + } } + /*int caveSiteInterval = 500; + foreach (Cave cave in Caves) + { + for (int x = cave.Area.X; x < cave.Area.Right; x += caveSiteInterval) + { + for (int y = cave.Area.Y; y < cave.Area.Bottom; y += caveSiteInterval) + { + int siteX = x + Rand.Int(caveSiteInterval / 2); + int siteY = y + Rand.Int(caveSiteInterval / 2); + + bool tooClose = false; + for (int i = 0; i cave.Tunnels.Contains(tunnel)))); } } GenerateWaypoints(tunnel, parentTunnel: tunnel.ParentTunnel); @@ -703,7 +763,12 @@ namespace Barotrauma { PositionsOfInterest[i] = new InterestingPosition( new Point(borders.Width - PositionsOfInterest[i].Position.X, PositionsOfInterest[i].Position.Y), - PositionsOfInterest[i].PositionType); + PositionsOfInterest[i].PositionType) + { + Submarine = PositionsOfInterest[i].Submarine, + Cave = PositionsOfInterest[i].Cave, + Ruin = PositionsOfInterest[i].Ruin, + }; } foreach (WayPoint waypoint in WayPoint.WayPointList) @@ -726,6 +791,7 @@ namespace Barotrauma cellGrid[x, y].Add(cell); } + float destructibleWallRatio = MathHelper.Lerp(0.2f, 1.0f, LevelData.Difficulty / 100.0f); foreach (Cave cave in Caves) { CreatePathToClosestTunnel(cave.StartPos); @@ -734,7 +800,7 @@ namespace Barotrauma caveCells.AddRange(cave.Tunnels.SelectMany(t => t.Cells)); foreach (var caveCell in caveCells) { - if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) < cave.CaveGenerationParams.DestructibleWallRatio) + if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) < destructibleWallRatio * cave.CaveGenerationParams.DestructibleWallRatio) { var chunk = CreateIceChunk(caveCell.Edges, caveCell.Center, health: 50.0f); if (chunk != null) @@ -755,7 +821,7 @@ namespace Barotrauma Ruins = new List(); for (int i = 0; i < GenerationParams.RuinCount; i++) { - GenerateRuin(mainPath.Cells, mirror); + GenerateRuin(mainPath, mirror); } EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); @@ -825,25 +891,30 @@ namespace Barotrauma }; foreach (Cave cave in Caves) { - cellBatches.Add(new Pair, Cave>(new List(), cave)); + var newCellBatch = new Pair, Cave>(new List(), cave); foreach (var caveCell in cave.Tunnels.SelectMany(t => t.Cells)) { foreach (var edge in caveCell.Edges) { if (!edge.NextToCave) { continue; } - if (edge.Cell1?.CellType == CellType.Solid && !cellBatches.Last().First.Contains(edge.Cell1)) + if (edge.Cell1?.CellType == CellType.Solid && !newCellBatch.First.Contains(edge.Cell1)) { - cellBatches.First().First.Remove(edge.Cell1); - cellBatches.Last().First.Add(edge.Cell1); + cellBatches.ForEach(cb => cb.First.Remove(edge.Cell1)); + newCellBatch.First.Add(edge.Cell1); } - if (edge.Cell2?.CellType == CellType.Solid && !cellBatches.Last().First.Contains(edge.Cell2)) + if (edge.Cell2?.CellType == CellType.Solid && !newCellBatch.First.Contains(edge.Cell2)) { - cellBatches.First().First.Remove(edge.Cell2); - cellBatches.Last().First.Add(edge.Cell2); + cellBatches.ForEach(cb => cb.First.Remove(edge.Cell2)); + newCellBatch.First.Add(edge.Cell2); } } } + if (newCellBatch.First.Any()) + { + cellBatches.Add(newCellBatch); + } } + cellBatches.RemoveAll(cb => !cb.First.Any()); Debug.Assert(cellsWithBody.Count == cellBatches.Sum(cb => cb.First.Count)); @@ -1140,8 +1211,9 @@ namespace Barotrauma private void GenerateWaypoints(Tunnel tunnel, Tunnel parentTunnel) { - List wayPoints = new List(); + if (tunnel.Cells.Count == 0) { return; } + List wayPoints = new List(); for (int i = 0; i < tunnel.Cells.Count; i++) { tunnel.Cells[i].CellType = CellType.Path; @@ -1228,10 +1300,8 @@ namespace Barotrauma private List GetTooCloseCells(List emptyCells, float minDistance) { List tooCloseCells = new List(); - if (minDistance <= 0.0f) { return tooCloseCells; } - - foreach (var cell in emptyCells) + foreach (var cell in emptyCells.Distinct()) { foreach (var tooCloseCell in GetTooCloseCells(cell.Center, minDistance)) { @@ -1241,25 +1311,13 @@ namespace Barotrauma } } } - - /*minDistance *= 0.5f; - do - { - tooCloseCells.AddRange(GetTooCloseCells(position, minDistance)); - - position += Vector2.Normalize(emptyCells[targetCellIndex].Center - position) * step; - - if (Vector2.Distance(emptyCells[targetCellIndex].Center, position) < step * 2.0f) targetCellIndex++; - - } while (Vector2.Distance(position, emptyCells[emptyCells.Count - 1].Center) > step * 2.0f);*/ - return tooCloseCells; } public List GetTooCloseCells(Vector2 position, float minDistance) { HashSet tooCloseCells = new HashSet(); - var closeCells = GetCells(position, 3); + var closeCells = GetCells(position, searchDepth: Math.Max((int)Math.Ceiling(minDistance / GridCellSize), 3)); float minDistSqr = minDistance * minDistance; foreach (VoronoiCell cell in closeCells) { @@ -1353,7 +1411,7 @@ namespace Barotrauma int padding = (int)(caveSize.X * 1.2f); Rectangle allowedArea = new Rectangle(padding, padding, Size.X - padding * 2, Size.Y - padding * 2); - var cavePos = FindPosAwayFromMainPath(radius, asFarAwayAsPossible: true, allowedArea); + var cavePos = FindPosAwayFromMainPath((parentTunnel.MinWidth + radius) * 1.2f, asCloseAsPossible: true, allowedArea); Point closestParentNode = parentTunnel.Nodes.First(); double closestDist = double.PositiveInfinity; @@ -1407,7 +1465,7 @@ namespace Barotrauma foreach (Tunnel branch in caveBranches) { - PositionsOfInterest.Add(new InterestingPosition(branch.Nodes.Last(), PositionType.Cave)); + PositionsOfInterest.Add(new InterestingPosition(branch.Nodes.Last(), PositionType.Cave, cave)); cave.Tunnels.Add(branch); } @@ -1426,7 +1484,7 @@ namespace Barotrauma } } - private void GenerateRuin(List mainPath, bool mirror) + private void GenerateRuin(Tunnel mainPath, bool mirror) { var ruinGenerationParams = RuinGenerationParams.GetRandom(); @@ -1435,12 +1493,12 @@ namespace Barotrauma Rand.Range(ruinGenerationParams.SizeMin.Y, ruinGenerationParams.SizeMax.Y, Rand.RandSync.Server)); int ruinRadius = Math.Max(ruinSize.X, ruinSize.Y) / 2; - Point ruinPos = FindPosAwayFromMainPath(ruinRadius + Tunnels.First().MinWidth, asFarAwayAsPossible: false, + Point ruinPos = FindPosAwayFromMainPath((ruinRadius + mainPath.MinWidth) * 1.2f, asCloseAsPossible: true, limits: new Rectangle(new Point(ruinSize.X / 2, ruinSize.Y / 2), Size - ruinSize)); VoronoiCell closestPathCell = null; double closestDist = 0.0f; - foreach (VoronoiCell pathCell in mainPath) + foreach (VoronoiCell pathCell in mainPath.Cells) { double dist = MathUtils.DistanceSquared(pathCell.Site.Coord.X, pathCell.Site.Coord.Y, ruinPos.X, ruinPos.Y); if (closestPathCell == null || dist < closestDist) @@ -1506,22 +1564,22 @@ namespace Barotrauma CreatePathToClosestTunnel(ruinPos); } - private Point FindPosAwayFromMainPath(double minDistance, bool asFarAwayAsPossible, Rectangle? limits = null) + private Point FindPosAwayFromMainPath(double minDistance, bool asCloseAsPossible, Rectangle? limits = null) { var validPoints = distanceField.FindAll(d => d.Second >= minDistance && (limits == null || limits.Value.Contains(d.First))); validPoints.RemoveAll(d => d.First.Y < GetBottomPosition(d.First.X).Y + minDistance); - if (asFarAwayAsPossible || !validPoints.Any()) + if (asCloseAsPossible || !validPoints.Any()) { if (!validPoints.Any()) { validPoints = distanceField; } - Pair furthestPoint = null; + Pair closestPoint = null; foreach (var point in validPoints) { - if (furthestPoint == null || point.Second > furthestPoint.Second) + if (closestPoint == null || point.Second < closestPoint.Second) { - furthestPoint = point; + closestPoint = point; } } - return furthestPoint.First; + return closestPoint.First; } else { @@ -1581,6 +1639,7 @@ namespace Barotrauma vertices.Add(edge.Point2); } } + if (vertices.Count < 3) { return null; } return CreateIceChunk(vertices.Select(v => v - position).ToList(), position, health); } @@ -1635,6 +1694,8 @@ namespace Barotrauma Vector2 edgeNormal = closestEdge.GetNormal(closestCell); float spireLength = (float)Math.Min(Math.Sqrt(closestDistSqr), maxLength); + spireLength *= MathHelper.Lerp(0.3f, 1.5f, Difficulty / 100.0f); + Vector2 extrudedPoint1 = closestEdge.Point1 + edgeNormal * spireLength * Rand.Range(0.8f, 1.0f, Rand.RandSync.Server); Vector2 extrudedPoint2 = closestEdge.Point2 + edgeNormal * spireLength * Rand.Range(0.8f, 1.0f, Rand.RandSync.Server); List vertices = new List() @@ -2101,7 +2162,7 @@ namespace Barotrauma if (resourcesInCluster < 1) { return false; } - PlaceResources(selectedPrefab, resourcesInCluster, location, out var placedResources, edgeLenght: edgeLength); + PlaceResources(selectedPrefab, resourcesInCluster, location, out var placedResources, edgeLength: edgeLength); itemCount += resourcesInCluster; location.InitializeResources(); location.Resources.AddRange(placedResources); @@ -2136,7 +2197,7 @@ namespace Barotrauma c.Equals(location) && c.Resources.Any(r => r != null && !r.Removed && (!(r.GetComponent() is Holdable h) || (h.Attachable && h.Attached))))); - if(locationHasResources) + if (locationHasResources) { allValidLocations.RemoveAt(i); } @@ -2231,10 +2292,10 @@ namespace Barotrauma } private void PlaceResources(ItemPrefab resourcePrefab, int resourceCount, ClusterLocation location, out List placedResources, - float? edgeLenght = null, float maxResourceOverlap = 0.4f) + float? edgeLength = null, float maxResourceOverlap = 0.4f) { - edgeLenght ??= Vector2.Distance(location.Edge.Point1, location.Edge.Point2); - var minResourceOverlap = -((edgeLenght.Value - (resourceCount * resourcePrefab.Size.X)) / (resourceCount * resourcePrefab.Size.X)); + edgeLength ??= Vector2.Distance(location.Edge.Point1, location.Edge.Point2); + var minResourceOverlap = -((edgeLength.Value - (resourceCount * resourcePrefab.Size.X)) / (resourceCount * resourcePrefab.Size.X)); minResourceOverlap = Math.Max(minResourceOverlap, 0.0f); var lerpAmounts = new float[resourceCount]; lerpAmounts[0] = 0.0f; @@ -2242,7 +2303,7 @@ namespace Barotrauma for (int i = 1; i < resourceCount; i++) { var overlap = Rand.Range(minResourceOverlap, maxResourceOverlap, sync: Rand.RandSync.Server); - lerpAmount += ((1.0f - overlap) * resourcePrefab.Size.X) / edgeLenght.Value; + lerpAmount += ((1.0f - overlap) * resourcePrefab.Size.X) / edgeLength.Value; lerpAmounts[i] = Math.Clamp(lerpAmount, 0.0f, 1.0f); } var startOffset = Rand.Range(0.0f, 1.0f - lerpAmount, sync: Rand.RandSync.Server); @@ -2252,7 +2313,9 @@ namespace Barotrauma Vector2 selectedPos = Vector2.Lerp(location.Edge.Point1, location.Edge.Point2, startOffset + lerpAmounts[i]); var item = new Item(resourcePrefab, selectedPos, submarine: null); Vector2 edgeNormal = location.Edge.GetNormal(location.Cell); - item.Move(edgeNormal * (item.body == null ? item.Rect.Height / 2 : ConvertUnits.ToDisplayUnits(item.body.GetMaxExtent() * 0.7f)), ignoreContacts: true); + float moveAmount = (item.body == null ? item.Rect.Height / 2 : ConvertUnits.ToDisplayUnits(item.body.GetMaxExtent() * 0.7f)); + moveAmount += (item.GetComponent()?.RandomOffsetFromWall ?? 0.0f) * Rand.Range(-0.5f, 0.5f, Rand.RandSync.Server); + item.Move(edgeNormal * moveAmount, ignoreContacts: true); if (item.GetComponent() is Holdable h) { h.AttachToWall(); @@ -2268,7 +2331,7 @@ namespace Barotrauma } } - public Vector2 GetRandomItemPos(PositionType spawnPosType, float randomSpread, float minDistFromSubs, float offsetFromWall = 10.0f) + public Vector2 GetRandomItemPos(PositionType spawnPosType, float randomSpread, float minDistFromSubs, float offsetFromWall = 10.0f, Func filter = null) { if (!PositionsOfInterest.Any()) { @@ -2280,7 +2343,7 @@ namespace Barotrauma int tries = 0; do { - TryGetInterestingPosition(true, spawnPosType, minDistFromSubs, out Vector2 startPos); + TryGetInterestingPosition(true, spawnPosType, minDistFromSubs, out Vector2 startPos, filter); Vector2 offset = Rand.Vector(Rand.Range(0.0f, randomSpread, Rand.RandSync.Server), Rand.RandSync.Server); if (!cells.Any(c => c.IsPointInside(startPos + offset))) @@ -2312,14 +2375,14 @@ namespace Barotrauma return position; } - public bool TryGetInterestingPosition(bool useSyncedRand, PositionType positionType, float minDistFromSubs, out Vector2 position) + public bool TryGetInterestingPosition(bool useSyncedRand, PositionType positionType, float minDistFromSubs, out Vector2 position, Func filter = null) { - bool success = TryGetInterestingPosition(useSyncedRand, positionType, minDistFromSubs, out Point pos); + bool success = TryGetInterestingPosition(useSyncedRand, positionType, minDistFromSubs, out Point pos, filter); position = pos.ToVector2(); return success; } - public bool TryGetInterestingPosition(bool useSyncedRand, PositionType positionType, float minDistFromSubs, out Point position) + public bool TryGetInterestingPosition(bool useSyncedRand, PositionType positionType, float minDistFromSubs, out Point position, Func filter = null) { if (!PositionsOfInterest.Any()) { @@ -2328,8 +2391,12 @@ namespace Barotrauma } List suitablePositions = PositionsOfInterest.FindAll(p => positionType.HasFlag(p.PositionType)); + if (filter != null) + { + suitablePositions.RemoveAll(p => !filter(p)); + } //avoid floating ice chunks on the main path - if (positionType == PositionType.MainPath || positionType == PositionType.SidePath) + if (positionType.HasFlag(PositionType.MainPath) || positionType.HasFlag(PositionType.SidePath)) { suitablePositions.RemoveAll(p => ExtraWalls.Any(w => w.Cells.Any(c => c.IsPointInside(p.Position.ToVector2())))); } @@ -2513,26 +2580,38 @@ namespace Barotrauma } cells.Remove(cell); - //if the edge is very short, remove an adjacent cell to prevent making the passage too narrow - if (Vector2.DistanceSquared(e.Point1, e.Point2) < 200.0f * 200.0f) + //go through the edges of this cell and find the ones that are next to a removed cell + foreach (var otherEdge in cell.Edges) { - foreach (GraphEdge e2 in cell.Edges) + var otherAdjacent = otherEdge.AdjacentCell(cell); + if (otherAdjacent == null || otherAdjacent.CellType == CellType.Solid) { continue; } + + //if the edge is very short, remove adjacent cells to prevent making the passage too narrow + if (Vector2.DistanceSquared(otherEdge.Point1, otherEdge.Point2) < 500.0f * 500.0f) { - if (e2 == e) { continue; } - var adjacentCell = e2.AdjacentCell(cell); - if (adjacentCell == null || adjacentCell.CellType == CellType.Removed) { continue; } - adjacentCell.CellType = CellType.Removed; - for (int x = 0; x < cellGrid.GetLength(0); x++) + foreach (GraphEdge e2 in cell.Edges) { - for (int y = 0; y < cellGrid.GetLength(1); y++) + if (e2 == otherEdge || e2 == otherEdge) { continue; } + if (!MathUtils.NearlyEqual(otherEdge.Point1, e2.Point1) && !MathUtils.NearlyEqual(otherEdge.Point2, e2.Point1) && !MathUtils.NearlyEqual(otherEdge.Point2, e2.Point2)) { - cellGrid[x, y].Remove(adjacentCell); + continue; } + var adjacentCell = e2.AdjacentCell(cell); + if (adjacentCell == null || adjacentCell.CellType == CellType.Removed) { continue; } + adjacentCell.CellType = CellType.Removed; + for (int x = 0; x < cellGrid.GetLength(0); x++) + { + for (int y = 0; y < cellGrid.GetLength(1); y++) + { + cellGrid[x, y].Remove(adjacentCell); + } + } + cells.Remove(adjacentCell); } - cells.Remove(adjacentCell); - break; } } + + break; } @@ -2639,7 +2718,7 @@ namespace Barotrauma { sub.ShowSonarMarker = false; sub.PhysicsBody.FarseerBody.BodyType = BodyType.Static; - sub.TeamID = Character.TeamType.None; + sub.TeamID = CharacterTeamType.None; } tempSW.Stop(); Debug.WriteLine($"Sub {sub.Info.Name} loaded in { tempSW.ElapsedMilliseconds} (ms)"); @@ -2833,6 +2912,12 @@ namespace Barotrauma { return true; } + if (Caves.Any(c => + ToolBox.GetWorldBounds(c.Area.Center, c.Area.Size).IntersectsWorld(bounds) || + ToolBox.GetWorldBounds(c.StartPos, new Point(1500)).IntersectsWorld(bounds))) + { + return true; + } return cells.Any(c => c.Body != null && Vector2.DistanceSquared(pos, c.Center) <= maxDistance && c.BodyVertices.Any(v => bounds.ContainsWorld(v))); } } @@ -3113,15 +3198,26 @@ namespace Barotrauma if (!(GameMain.NetworkMember?.IsClient ?? false)) { //empty the reactor - foreach (Item item in reactorContainer.Inventory.Items) + foreach (Item item in reactorContainer.Inventory.AllItems) { - if (item == null) { continue; } + if (item.NonInteractable) { continue; } Entity.Spawner.AddToRemoveQueue(item); } //remove wires foreach (Item item in beaconItems.Where(it => it.GetComponent() != null).ToList()) { + if (item.NonInteractable) { continue; } + Wire wire = item.GetComponent(); + if (wire.Locked) { continue; } + if (wire.Connections[0] != null && (wire.Connections[0].Item.NonInteractable || wire.Connections[0].Item.GetComponent().Locked)) + { + continue; + } + if (wire.Connections[1] != null && (wire.Connections[1].Item.NonInteractable || wire.Connections[1].Item.GetComponent().Locked)) + { + continue; + } if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < 0.25f) { Entity.Spawner.AddToRemoveQueue(item); @@ -3131,6 +3227,7 @@ namespace Barotrauma //break powered items foreach (Item item in beaconItems.Where(it => it.Components.Any(c => c is Powered))) { + if (item.NonInteractable) { continue; } if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < 0.5f) { item.Condition *= Rand.Range(0.2f, 0.6f, Rand.RandSync.Unsynced); @@ -3221,7 +3318,7 @@ namespace Barotrauma var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: job, randSync: Rand.RandSync.Server); var corpse = Character.Create(CharacterPrefab.HumanConfigFile, worldPos, ToolBox.RandomSeed(8), characterInfo, hasAi: true, createNetworkEvent: true); corpse.AnimController.FindHull(worldPos, true); - corpse.TeamID = Character.TeamType.None; + corpse.TeamID = CharacterTeamType.None; corpse.EnableDespawn = false; selectedPrefab.GiveItems(corpse, wreck); corpse.Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null, log: false); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs index ac0896f4e..d18b5cb66 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs @@ -35,6 +35,11 @@ namespace Barotrauma public readonly int InitialDepth; + /// + /// Determined during level generation based on the size of the submarine. Null if the level hasn't been generated. + /// + public int? MinMainPathWidth; + public readonly List EventHistory = new List(); public readonly List NonRepeatableEvents = new List(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObject.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObject.cs index 510d21414..652a65e26 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObject.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObject.cs @@ -52,7 +52,11 @@ namespace Barotrauma public Sprite Sprite { - get { return spriteIndex < 0 || Prefab.Sprites.Count == 0 ? null : Prefab.Sprites[spriteIndex % Prefab.Sprites.Count]; } + get + { + var prefab = ActivePrefab?.Sprites.Count > 0 ? ActivePrefab : Prefab; + return spriteIndex < 0 || prefab.Sprites.Count == 0 ? null : prefab.Sprites[spriteIndex % prefab.Sprites.Count]; + } } Vector2 ISpatialEntity.Position => new Vector2(Position.X, Position.Y); @@ -63,6 +67,8 @@ namespace Barotrauma public Submarine Submarine => null; + public Level.Cave ParentCave; + public LevelObject(LevelObjectPrefab prefab, Vector3 position, float scale, float rotation = 0.0f) { ActivePrefab = Prefab = prefab; @@ -110,6 +116,19 @@ namespace Barotrauma Triggers.Add(newTrigger); } + if (spriteIndex == -1) + { + foreach (var overrideProperties in prefab.OverrideProperties) + { + if (overrideProperties == null) { continue; } + if (overrideProperties.Sprites.Count > 0) + { + spriteIndex = Rand.Int(overrideProperties.Sprites.Count, Rand.RandSync.Server); + break; + } + } + } + NeedsUpdate = NeedsNetworkSyncing || (Triggers != null && Triggers.Any()) || Prefab.PhysicsBodyTriggerIndex > -1; InitProjSpecific(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs index 4790612b9..238743d34 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -171,7 +171,7 @@ namespace Barotrauma for (int i = 0; i < cave.CaveGenerationParams.LevelObjectAmount; i++) { //get a random prefab and find a place to spawn it - LevelObjectPrefab prefab = GetRandomPrefab(cave.CaveGenerationParams, availablePrefabs); + LevelObjectPrefab prefab = GetRandomPrefab(cave.CaveGenerationParams, availablePrefabs, requireCaveSpecificOverride: true); if (prefab == null) { continue; } if (!suitableSpawnPositions.ContainsKey(prefab)) { @@ -184,10 +184,10 @@ namespace Barotrauma } SpawnPosition spawnPosition = ToolBox.SelectWeightedRandom(suitableSpawnPositions[prefab], spawnPositionWeights[prefab], Rand.RandSync.Server); if (spawnPosition == null && prefab.SpawnPos != LevelObjectPrefab.SpawnPosType.None) { continue; } - PlaceObject(prefab, spawnPosition, level); + PlaceObject(prefab, spawnPosition, level, cave); if (prefab.MaxCount < amount) { - if (objects.Count(o => o.Prefab == prefab) >= prefab.MaxCount) + if (objects.Count(o => o.Prefab == prefab && o.ParentCave == cave) >= prefab.MaxCount) { availablePrefabs.Remove(prefab); } @@ -196,7 +196,51 @@ namespace Barotrauma } } - private void PlaceObject(LevelObjectPrefab prefab, SpawnPosition spawnPosition, Level level) + public void PlaceNestObjects(Level level, Level.Cave cave, Vector2 nestPosition, float nestRadius, int objectAmount) + { + Rand.SetSyncedSeed(ToolBox.StringToInt(level.Seed)); + + var availablePrefabs = new List(LevelObjectPrefab.List.FindAll(p => p.SpawnPos.HasFlag(LevelObjectPrefab.SpawnPosType.NestWall))); + Dictionary> suitableSpawnPositions = new Dictionary>(); + Dictionary> spawnPositionWeights = new Dictionary>(); + + List availableSpawnPositions = new List(); + var caveCells = cave.Tunnels.SelectMany(t => t.Cells); + List caveWallCells = new List(); + foreach (var edge in caveCells.SelectMany(c => c.Edges)) + { + if (!edge.NextToCave) { continue; } + if (MathUtils.LineSegmentToPointDistanceSquared(edge.Point1.ToPoint(), edge.Point2.ToPoint(), nestPosition.ToPoint()) > nestRadius * nestRadius) { continue; } + if (edge.Cell1?.CellType == CellType.Solid) { caveWallCells.Add(edge.Cell1); } + if (edge.Cell2?.CellType == CellType.Solid) { caveWallCells.Add(edge.Cell2); } + } + availableSpawnPositions.AddRange(GetAvailableSpawnPositions(caveWallCells.Distinct(), LevelObjectPrefab.SpawnPosType.CaveWall)); + + for (int i = 0; i < objectAmount; i++) + { + //get a random prefab and find a place to spawn it + LevelObjectPrefab prefab = GetRandomPrefab(cave.CaveGenerationParams, availablePrefabs, requireCaveSpecificOverride: false); + if (prefab == null) { continue; } + if (!suitableSpawnPositions.ContainsKey(prefab)) + { + suitableSpawnPositions.Add(prefab, + availableSpawnPositions.Where(sp => + sp.Length >= prefab.MinSurfaceWidth && + (sp.Alignment == Alignment.Any || prefab.Alignment.HasFlag(sp.Alignment))).ToList()); + spawnPositionWeights.Add(prefab, + suitableSpawnPositions[prefab].Select(sp => sp.GetSpawnProbability(prefab)).ToList()); + } + SpawnPosition spawnPosition = ToolBox.SelectWeightedRandom(suitableSpawnPositions[prefab], spawnPositionWeights[prefab], Rand.RandSync.Server); + if (spawnPosition == null && prefab.SpawnPos != LevelObjectPrefab.SpawnPosType.None) { continue; } + PlaceObject(prefab, spawnPosition, level); + if (objects.Count(o => o.Prefab == prefab) >= prefab.MaxCount) + { + availablePrefabs.Remove(prefab); + } + } + } + + private void PlaceObject(LevelObjectPrefab prefab, SpawnPosition spawnPosition, Level level, Level.Cave parentCave = null) { float rotation = 0.0f; if (prefab.AlignWithSurface && spawnPosition.Normal.LengthSquared() > 0.001f && spawnPosition != null) @@ -228,6 +272,7 @@ namespace Barotrauma var newObject = new LevelObject(prefab, new Vector3(position, Rand.Range(prefab.DepthRange.X, prefab.DepthRange.Y, Rand.RandSync.Server)), Rand.Range(prefab.MinSize, prefab.MaxSize, Rand.RandSync.Server), rotation); AddObject(newObject, level); + newObject.ParentCave = parentCave; foreach (LevelObjectPrefab.ChildObject child in prefab.ChildObjects) { @@ -237,7 +282,7 @@ namespace Barotrauma var matchingPrefabs = LevelObjectPrefab.List.Where(p => child.AllowedNames.Contains(p.Name)); int prefabCount = matchingPrefabs.Count(); var childPrefab = prefabCount == 0 ? null : matchingPrefabs.ElementAt(Rand.Range(0, prefabCount, Rand.RandSync.Server)); - if (childPrefab == null) continue; + if (childPrefab == null) { continue; } Vector2 childPos = position + edgeDir * Rand.Range(-0.5f, 0.5f, Rand.RandSync.Server) * prefab.MinSurfaceWidth; @@ -247,6 +292,7 @@ namespace Barotrauma rotation + Rand.Range(childPrefab.RandomRotationRad.X, childPrefab.RandomRotationRad.Y, Rand.RandSync.Server)); AddObject(childObject, level); + childObject.ParentCave = parentCave; } } } @@ -402,7 +448,7 @@ namespace Barotrauma return objectsInRange; } - private List GetAvailableSpawnPositions(IEnumerable cells, LevelObjectPrefab.SpawnPosType spawnPosType) + private List GetAvailableSpawnPositions(IEnumerable cells, LevelObjectPrefab.SpawnPosType spawnPosType, bool checkFlags = true) { List spawnPosTypes = new List(4); List availableSpawnPositions = new List(); @@ -498,12 +544,12 @@ namespace Barotrauma availablePrefabs.Select(p => p.GetCommonness(generationParams)).ToList(), Rand.RandSync.Server); } - private LevelObjectPrefab GetRandomPrefab(CaveGenerationParams caveParams, IList availablePrefabs) + private LevelObjectPrefab GetRandomPrefab(CaveGenerationParams caveParams, IList availablePrefabs, bool requireCaveSpecificOverride) { - if (availablePrefabs.Sum(p => p.GetCommonness(caveParams)) <= 0.0f) { return null; } + if (availablePrefabs.Sum(p => p.GetCommonness(caveParams, requireCaveSpecificOverride)) <= 0.0f) { return null; } return ToolBox.SelectWeightedRandom( availablePrefabs, - availablePrefabs.Select(p => p.GetCommonness(caveParams)).ToList(), Rand.RandSync.Server); + availablePrefabs.Select(p => p.GetCommonness(caveParams, requireCaveSpecificOverride)).ToList(), Rand.RandSync.Server); } public override void Remove() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs index 552d90a9c..139837000 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs @@ -37,11 +37,12 @@ namespace Barotrauma MainPathWall = 1, SidePathWall = 2, CaveWall = 4, - RuinWall = 8, - SeaFloor = 16, - MainPath = 32, - LevelStart = 64, - LevelEnd = 128, + NestWall = 8, + RuinWall = 16, + SeaFloor = 32, + MainPath = 64, + LevelStart = 128, + LevelEnd = 256, Wall = MainPathWall | SidePathWall | CaveWall, } @@ -442,14 +443,14 @@ namespace Barotrauma partial void InitProjSpecific(XElement element); - public float GetCommonness(CaveGenerationParams generationParams) + public float GetCommonness(CaveGenerationParams generationParams, bool requireCaveSpecificOverride = true) { if (generationParams?.Identifier != null && OverrideCommonness.TryGetValue(generationParams.Identifier, out float commonness)) { return commonness; } - return 0.0f; + return requireCaveSpecificOverride ? 0.0f : Commonness; } public float GetCommonness(LevelGenerationParams generationParams) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs index 171ae1b46..3fbe4be14 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs @@ -34,44 +34,38 @@ namespace Barotrauma public Action OnTriggered; - private PhysicsBody physicsBody; - /// /// Effects applied to entities that are inside the trigger /// - private List statusEffects = new List(); + private readonly List statusEffects = new List(); /// /// Attacks applied to entities that are inside the trigger /// - private List attacks = new List(); + private readonly List attacks = new List(); - private float cameraShake; + private readonly float cameraShake; private Vector2 unrotatedForce; private float forceFluctuationTimer, currentForceFluctuation = 1.0f; - private HashSet triggerers = new HashSet(); + private readonly HashSet triggerers = new HashSet(); - private TriggererType triggeredBy; + private readonly TriggererType triggeredBy; - private float randomTriggerInterval; - private float randomTriggerProbability; + private readonly float randomTriggerInterval; + private readonly float randomTriggerProbability; private float randomTriggerTimer; private float triggeredTimer; - - //how far away this trigger can activate other triggers from - private float triggerOthersDistance; - - private HashSet tags = new HashSet(); + private readonly HashSet tags = new HashSet(); //other triggers have to have at least one of these tags to trigger this one - private HashSet allowedOtherTriggerTags = new HashSet(); + private readonly HashSet allowedOtherTriggerTags = new HashSet(); /// /// How long the trigger stays in the triggered state after triggerers have left /// - private float stayTriggeredDelay; + private readonly float stayTriggeredDelay; public LevelTrigger ParentTrigger; @@ -88,30 +82,24 @@ namespace Barotrauma set { worldPosition = value; - physicsBody?.SetTransform(ConvertUnits.ToSimUnits(value), physicsBody.Rotation); + PhysicsBody?.SetTransform(ConvertUnits.ToSimUnits(value), PhysicsBody.Rotation); } } public float Rotation { - get { return physicsBody == null ? 0.0f : physicsBody.Rotation; } + get { return PhysicsBody == null ? 0.0f : PhysicsBody.Rotation; } set { - if (physicsBody == null) return; - physicsBody.SetTransform(physicsBody.Position, value); + if (PhysicsBody == null) return; + PhysicsBody.SetTransform(PhysicsBody.Position, value); CalculateDirectionalForce(); } } - public PhysicsBody PhysicsBody - { - get { return physicsBody; } - } + public PhysicsBody PhysicsBody { get; private set; } - public float TriggerOthersDistance - { - get { return triggerOthersDistance; } - } + public float TriggerOthersDistance { get; private set; } public IEnumerable Triggerers { @@ -153,7 +141,7 @@ namespace Barotrauma private set; } - private TriggerForceMode forceMode; + private readonly TriggerForceMode forceMode; public TriggerForceMode ForceMode { get { return forceMode; } @@ -198,6 +186,9 @@ namespace Barotrauma get; set; } + + private bool triggeredOnce; + private readonly bool triggerOnce; public LevelTrigger(XElement element, Vector2 position, float rotation, float scale = 1.0f, string parentDebugName = "") { @@ -206,20 +197,20 @@ namespace Barotrauma worldPosition = position; if (element.Attributes("radius").Any() || element.Attributes("width").Any() || element.Attributes("height").Any()) { - physicsBody = new PhysicsBody(element, scale) + PhysicsBody = new PhysicsBody(element, scale) { CollisionCategories = Physics.CollisionLevel, CollidesWith = Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionProjectile | Physics.CollisionWall }; - physicsBody.FarseerBody.OnCollision += PhysicsBody_OnCollision; - physicsBody.FarseerBody.OnSeparation += PhysicsBody_OnSeparation; - physicsBody.FarseerBody.SetIsSensor(true); - physicsBody.FarseerBody.BodyType = BodyType.Static; - physicsBody.FarseerBody.BodyType = BodyType.Kinematic; + PhysicsBody.FarseerBody.OnCollision += PhysicsBody_OnCollision; + PhysicsBody.FarseerBody.OnSeparation += PhysicsBody_OnSeparation; + PhysicsBody.FarseerBody.SetIsSensor(true); + PhysicsBody.FarseerBody.BodyType = BodyType.Static; + PhysicsBody.FarseerBody.BodyType = BodyType.Kinematic; ColliderRadius = ConvertUnits.ToDisplayUnits(Math.Max(Math.Max(PhysicsBody.radius, PhysicsBody.width / 2.0f), PhysicsBody.height / 2.0f)); - physicsBody.SetTransform(ConvertUnits.ToSimUnits(position), rotation); + PhysicsBody.SetTransform(ConvertUnits.ToSimUnits(position), rotation); } cameraShake = element.GetAttributeFloat("camerashake", 0.0f); @@ -227,6 +218,8 @@ namespace Barotrauma InfectIdentifier = element.GetAttributeString("infectidentifier", null); InfectionChance = element.GetAttributeFloat("infectionchance", 0.05f); + triggerOnce = element.GetAttributeBool("triggeronce", false); + stayTriggeredDelay = element.GetAttributeFloat("staytriggereddelay", 0.0f); randomTriggerInterval = element.GetAttributeFloat("randomtriggerinterval", 0.0f); randomTriggerProbability = element.GetAttributeFloat("randomtriggerprobability", 0.0f); @@ -256,7 +249,7 @@ namespace Barotrauma DebugConsole.ThrowError("Error in LevelTrigger config: \"" + triggeredByStr + "\" is not a valid triggerer type."); } UpdateCollisionCategories(); - triggerOthersDistance = element.GetAttributeFloat("triggerothersdistance", 0.0f); + TriggerOthersDistance = element.GetAttributeFloat("triggerothersdistance", 0.0f); var tagsArray = element.GetAttributeStringArray("tags", new string[0]); foreach (string tag in tagsArray) @@ -283,11 +276,14 @@ namespace Barotrauma case "attack": case "damage": var attack = new Attack(subElement, string.IsNullOrEmpty(parentDebugName) ? "LevelTrigger" : "LevelTrigger in " + parentDebugName); - var multipliedAfflictions = attack.GetMultipliedAfflictions((float)Timing.Step); - attack.Afflictions.Clear(); - foreach (Affliction affliction in multipliedAfflictions) + if (!triggerOnce) { - attack.Afflictions.Add(affliction, null); + var multipliedAfflictions = attack.GetMultipliedAfflictions((float)Timing.Step); + attack.Afflictions.Clear(); + foreach (Affliction affliction in multipliedAfflictions) + { + attack.Afflictions.Add(affliction, null); + } } attacks.Add(attack); break; @@ -300,14 +296,14 @@ namespace Barotrauma private void UpdateCollisionCategories() { - if (physicsBody == null) return; + if (PhysicsBody == null) return; var collidesWith = Physics.CollisionNone; - if (triggeredBy.HasFlag(TriggererType.Character) || triggeredBy.HasFlag(TriggererType.Creature)) collidesWith |= Physics.CollisionCharacter; - if (triggeredBy.HasFlag(TriggererType.Item)) collidesWith |= Physics.CollisionItem | Physics.CollisionProjectile; - if (triggeredBy.HasFlag(TriggererType.Submarine)) collidesWith |= Physics.CollisionWall; + if (triggeredBy.HasFlag(TriggererType.Human) || triggeredBy.HasFlag(TriggererType.Creature)) { collidesWith |= Physics.CollisionCharacter; } + if (triggeredBy.HasFlag(TriggererType.Item)) { collidesWith |= Physics.CollisionItem | Physics.CollisionProjectile; } + if (triggeredBy.HasFlag(TriggererType.Submarine)) { collidesWith |= Physics.CollisionWall; } - physicsBody.CollidesWith = collidesWith; + PhysicsBody.CollidesWith = collidesWith; } private void CalculateDirectionalForce() @@ -362,7 +358,7 @@ namespace Barotrauma private void PhysicsBody_OnSeparation(Fixture fixtureA, Fixture fixtureB, Contact contact) { Entity entity = GetEntity(fixtureB); - if (entity == null) return; + if (entity == null) { return; } if (entity is Character character && (!character.Enabled || character.Removed) && @@ -376,22 +372,25 @@ namespace Barotrauma //check if there are contacts with any other fixture of the trigger //(the OnSeparation callback happens when two fixtures separate, //e.g. if a body stops touching the circular fixture at the end of a capsule-shaped body) - ContactEdge contactEdge = fixtureA.Body.ContactList; - while (contactEdge != null) + foreach (Fixture fixture in PhysicsBody.FarseerBody.FixtureList) { - if (contactEdge.Contact != null && - contactEdge.Contact.Enabled && - contactEdge.Contact.IsTouching) + ContactEdge contactEdge = fixture.Body.ContactList; + while (contactEdge != null) { - if (contactEdge.Contact.FixtureA != fixtureA && contactEdge.Contact.FixtureB != fixtureA) + if (contactEdge.Contact != null && + contactEdge.Contact.Enabled && + contactEdge.Contact.IsTouching) { - var otherEntity = GetEntity(contactEdge.Contact.FixtureB == fixtureB ? - contactEdge.Contact.FixtureB : - contactEdge.Contact.FixtureA); - if (otherEntity == entity) { return; } + if (contactEdge.Contact.FixtureA != fixture && contactEdge.Contact.FixtureB != fixture) + { + var otherEntity = GetEntity(contactEdge.Contact.FixtureB == fixtureB ? + contactEdge.Contact.FixtureB : + contactEdge.Contact.FixtureA); + if (otherEntity == entity) { return; } + } } + contactEdge = contactEdge.Next; } - contactEdge = contactEdge.Next; } if (triggerers.Contains(entity)) @@ -403,10 +402,10 @@ namespace Barotrauma private Entity GetEntity(Fixture fixture) { - if (fixture.Body == null || fixture.Body.UserData == null) return null; - if (fixture.Body.UserData is Entity entity) return entity; - if (fixture.Body.UserData is Limb limb) return limb.character; - if (fixture.Body.UserData is SubmarineBody subBody) return subBody.Submarine; + if (fixture.Body == null || fixture.Body.UserData == null) { return null; } + if (fixture.Body.UserData is Entity entity) { return entity; } + if (fixture.Body.UserData is Limb limb) { return limb.character; } + if (fixture.Body.UserData is SubmarineBody subBody) { return subBody.Submarine; } return null; } @@ -416,15 +415,15 @@ namespace Barotrauma /// public void OtherTriggered(LevelObject levelObject, LevelTrigger otherTrigger) { - if (!triggeredBy.HasFlag(TriggererType.OtherTrigger) || stayTriggeredDelay <= 0.0f) return; + if (!triggeredBy.HasFlag(TriggererType.OtherTrigger) || stayTriggeredDelay <= 0.0f) { return; } //check if the other trigger has appropriate tags if (allowedOtherTriggerTags.Count > 0) { - if (!allowedOtherTriggerTags.Any(t => otherTrigger.tags.Contains(t))) return; + if (!allowedOtherTriggerTags.Any(t => otherTrigger.tags.Contains(t))) { return; } } - if (Vector2.DistanceSquared(WorldPosition, otherTrigger.WorldPosition) <= otherTrigger.triggerOthersDistance * otherTrigger.triggerOthersDistance) + if (Vector2.DistanceSquared(WorldPosition, otherTrigger.WorldPosition) <= otherTrigger.TriggerOthersDistance * otherTrigger.TriggerOthersDistance) { bool wasAlreadyTriggered = IsTriggered; triggeredTimer = stayTriggeredDelay; @@ -441,10 +440,10 @@ namespace Barotrauma triggerers.RemoveWhere(t => t.Removed); - if (physicsBody != null) + if (PhysicsBody != null) { //failsafe to ensure triggerers get removed when they're far from the trigger - float maxExtent = Math.Max(ConvertUnits.ToDisplayUnits(physicsBody.GetMaxExtent() * 5), 5000.0f); + float maxExtent = Math.Max(ConvertUnits.ToDisplayUnits(PhysicsBody.GetMaxExtent() * 5), 5000.0f); triggerers.RemoveWhere(t => { return Vector2.Distance(t.WorldPosition, WorldPosition) > maxExtent; @@ -500,17 +499,43 @@ namespace Barotrauma } } + if (triggerOnce) + { + if (triggeredOnce) { return; } + if (triggerers.Count > 0) { triggeredOnce = true; } + } + foreach (Entity triggerer in triggerers) { foreach (StatusEffect effect in statusEffects) { - if (triggerer is Character) + Vector2? position = null; + if (effect.HasTargetType(StatusEffect.TargetType.This)) { position = WorldPosition; } + if (triggerer is Character character) { - effect.Apply(effect.type, deltaTime, triggerer, (Character)triggerer); + effect.Apply(effect.type, deltaTime, triggerer, character, position); + if (effect.HasTargetType(StatusEffect.TargetType.Contained) && character.Inventory != null) + { + foreach (Item item in character.Inventory.AllItemsMod) + { + if (item.ContainedItems == null) { continue; } + foreach (Item containedItem in item.ContainedItems) + { + effect.Apply(effect.type, deltaTime, triggerer, containedItem.AllPropertyObjects, position); + } + } + } } - else if (triggerer is Item) + else if (triggerer is Item item) { - effect.Apply(effect.type, deltaTime, triggerer, ((Item)triggerer).AllPropertyObjects); + effect.Apply(effect.type, deltaTime, triggerer, item.AllPropertyObjects, position); + } + if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) || + effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) + { + var targets = new List(); + effect.GetNearbyTargets(worldPosition, targets); + effect.Apply(effect.type, deltaTime, triggerer, targets); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs index 1b15bb666..d84956923 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs @@ -1,9 +1,14 @@ -using Barotrauma.IO; -using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; +#if DEBUG +using System.Xml; +#else +using Barotrauma.IO; +#endif + namespace Barotrauma.RuinGeneration { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs index 2e0acdcf5..52e78853e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; +using Barotrauma.IO; namespace Barotrauma { @@ -98,6 +99,8 @@ namespace Barotrauma LinkedSubmarine sl = CreateDummy(mainSub, doc.Root, position); sl.filePath = filePath; + sl.saveElement = doc.Root; + sl.saveElement.Name = "LinkedSubmarine"; return sl; } @@ -132,7 +135,11 @@ namespace Barotrauma public override MapEntity Clone() { - return CreateDummy(Submarine, filePath, Position); + XElement cloneElement = new XElement(saveElement); + LinkedSubmarine sl = CreateDummy(Submarine, cloneElement, Position); + sl.saveElement = cloneElement; + sl.filePath = filePath; + return sl; } private void GenerateWallVertices(XElement rootElement) @@ -232,6 +239,7 @@ namespace Barotrauma IdRemap parentRemap = new IdRemap(Submarine.Info.SubmarineElement, Submarine.IdOffset); sub = Submarine.Load(info, false, parentRemap); + sub.Info.SubmarineClass = Submarine.Info.SubmarineClass; IdRemap childRemap = new IdRemap(saveElement, sub.IdOffset); @@ -290,6 +298,7 @@ namespace Barotrauma originalMyPortID = myPort.Item.ID; myPort.Undock(); + myPort.DockingDir = 0; //something else is already docked to the port this sub should be docked to //may happen if a shuttle is lost, another vehicle docked to where the shuttle used to be, @@ -324,7 +333,7 @@ namespace Barotrauma if (wall.Submarine != sub) { continue; } for (int i = 0; i < wall.SectionCount; i++) { - wall.AddDamage(i, -wall.MaxHealth); + wall.SetDamage(i, 0, createNetworkEvent: false); } } foreach (Hull hull in Hull.hullList) @@ -349,15 +358,15 @@ namespace Barotrauma { var doc = SubmarineInfo.OpenFile(filePath); saveElement = doc.Root; - saveElement.Name = "LinkedSubmarine"; saveElement.Add(new XAttribute("filepath", filePath)); } else { saveElement = this.saveElement; } + saveElement.Name = "LinkedSubmarine"; - if (saveElement.Attribute("pos") != null) saveElement.Attribute("pos").Remove(); + if (saveElement.Attribute("pos") != null) { saveElement.Attribute("pos").Remove(); } saveElement.Add(new XAttribute("pos", XMLExtensions.Vector2ToString(Position - Submarine.HiddenSubPosition))); var linkedPort = linkedTo.FirstOrDefault(lt => (lt is Item) && ((Item)lt).GetComponent() != null); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index a3c2000c4..0b4c4e441 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -62,7 +62,9 @@ namespace Barotrauma public bool Discovered; - public int TypeChangeTimer; + public readonly Dictionary ProximityTimer = new Dictionary(); + + public Pair PendingLocationTypeChange; public string BaseName { get => baseName; } @@ -80,12 +82,74 @@ namespace Barotrauma public Reputation Reputation { get; set; } + #region Store + private const float StoreMaxReputationModifier = 0.1f; private const float StoreSellPriceModifier = 0.8f; - private const float MechanicalMaxDiscountPercentage = 50.0f; + private const float DailySpecialPriceModifier = 0.9f; + private const float RequestGoodPriceModifier = 1.5f; public const int StoreInitialBalance = 5000; - public int StoreCurrentBalance { get; set; } + /// + /// In percentages + /// + private const int StorePriceModifierRange = 5; + /// + /// In percentages. Larger values make buying more expensive and selling less profitable, and vice versa. + /// + public int StorePriceModifier { get; private set; } + + public Color BalanceColor => ActiveStoreBalanceStatus.Color; + public StoreBalanceStatus ActiveStoreBalanceStatus { get; private set; } + private static StoreBalanceStatus DefaultBalanceStatus { get; } = new StoreBalanceStatus(1.0f, 1.0f, Color.White); + private static List StoreBalanceStatuses { get; } = new List + { + new StoreBalanceStatus(0.5f, 0.75f, Color.Orange), + new StoreBalanceStatus(0.25f, 0.2f, Color.Red), + }; + + public struct StoreBalanceStatus + { + public float PercentageOfInitialBalance { get; } + public float SellPriceModifier { get; } + public Color Color { get; } + + public StoreBalanceStatus(float percentage, float sellPriceModifier, Color color) + { + PercentageOfInitialBalance = percentage; + SellPriceModifier = sellPriceModifier; + Color = color; + } + } + + private int storeCurrentBalance; + public int StoreCurrentBalance + { + get + { + return storeCurrentBalance; + } + set + { + storeCurrentBalance = value; + ActiveStoreBalanceStatus = GetStoreBalanceStatus(value); + } + } + public List StoreStock { get; set; } + public List DailySpecials { get; } = new List(); + public List RequestedGoods { get; } = new List(); + + /// + /// How many map progress steps it takes before the discounts should be updated. + /// + private const int SpecialsUpdateInterval = 3; + private const int DailySpecialsCount = 3; + private const int RequestedGoodsCount = 3; + private int StepsSinceSpecialsUpdated { get; set; } + + #endregion + + private const float MechanicalMaxDiscountPercentage = 50.0f; private readonly List takenItems = new List(); public IEnumerable TakenItems @@ -150,6 +214,8 @@ namespace Barotrauma public string LastTypeChangeMessage; + public int TimeSinceLastTypeChange; + private struct LoadedMission { public MissionPrefab MissionPrefab { get; } @@ -189,10 +255,23 @@ namespace Barotrauma baseName = element.GetAttributeString("basename", ""); Name = element.GetAttributeString("name", ""); MapPosition = element.GetAttributeVector2("position", Vector2.Zero); - TypeChangeTimer = element.GetAttributeInt("changetimer", 0); Discovered = element.GetAttributeBool("discovered", false); PriceMultiplier = element.GetAttributeFloat("pricemultiplier", 1.0f); - MechanicalPriceMultiplier = element.GetAttributeFloat("mechanicalpricemultipler", 1.0f); + MechanicalPriceMultiplier = element.GetAttributeFloat("mechanicalpricemultipler", 1.0f); + TimeSinceLastTypeChange = element.GetAttributeInt("timesincelasttypechange", 0); + + for (int i = 0; i < Type.CanChangeTo.Count; i++) + { + ProximityTimer.Add(Type.CanChangeTo[i], element.GetAttributeInt("proximitytimer" + i, 0)); + } + + int locationTypeChangeIndex = element.GetAttributeInt("pendinglocationtypechange", -1); + if (locationTypeChangeIndex > 0 && locationTypeChangeIndex < Type.CanChangeTo.Count - 1) + { + PendingLocationTypeChange = new Pair( + Type.CanChangeTo[locationTypeChangeIndex], + element.GetAttributeInt("pendinglocationtypechangetimer", 0)); + } string[] takenItemStr = element.GetAttributeStringArray("takenitems", new string[0]); foreach (string takenItem in takenItemStr) @@ -232,13 +311,8 @@ namespace Barotrauma LevelData = new LevelData(element.Element("Level")); PortraitId = ToolBox.StringToInt(Name); - - if (element.GetChildElement("store") is XElement storeElement) - { - StoreCurrentBalance = storeElement.GetAttributeInt("balance", StoreInitialBalance); - StoreStock = LoadStoreStock(storeElement); - } + LoadStore(element); LoadMissions(element); } @@ -456,6 +530,59 @@ namespace Barotrauma return type.NameFormats[nameFormatIndex].Replace("[name]", baseName); } + public void LoadStore(XElement locationElement) + { + StoreStock?.Clear(); + DailySpecials.Clear(); + RequestedGoods.Clear(); + + if (locationElement.GetChildElement("store") is XElement storeElement) + { + StoreCurrentBalance = storeElement.GetAttributeInt("balance", StoreInitialBalance); + StorePriceModifier = storeElement.GetAttributeInt("pricemodifier", 0); + + StoreStock ??= new List(); + foreach (XElement stockElement in storeElement.GetChildElements("stock")) + { + var id = stockElement.GetAttributeString("id", null); + if (string.IsNullOrWhiteSpace(id)) { continue; } + var prefab = ItemPrefab.Prefabs.Find(p => p.Identifier == id); + if (prefab == null) { continue; } + var qty = stockElement.GetAttributeInt("qty", 0); + if (qty < 1) { continue; } + StoreStock.Add(new PurchasedItem(prefab, qty)); + } + + StepsSinceSpecialsUpdated = storeElement.GetAttributeInt("stepssincespecialsupdated", 0); + + if (storeElement.GetChildElement("dailyspecials") is XElement specialsElement) + { + var loadedDailySpecials = LoadStoreSpecials(specialsElement); + DailySpecials.AddRange(loadedDailySpecials); + } + + if (storeElement.GetChildElement("requestedgoods") is XElement goodsElement) + { + var loadedRequestedGoods = LoadStoreSpecials(goodsElement); + RequestedGoods.AddRange(loadedRequestedGoods); + } + + static List LoadStoreSpecials(XElement element) + { + List specials = new List(); + foreach (var childElement in element.GetChildElements("item")) + { + var id = childElement.GetAttributeString("id", null); + if (string.IsNullOrWhiteSpace(id)) { continue; } + var prefab = ItemPrefab.Find(null, id); + if (prefab == null) { continue; } + specials.Add(prefab); + } + return specials; + } + } + } + private List CreateStoreStock() { var stock = new List(); @@ -463,31 +590,28 @@ namespace Barotrauma { if (prefab.CanBeBoughtAtLocation(this, out PriceInfo priceInfo)) { - var quantity = priceInfo.MinAvailableAmount > 0 ? priceInfo.MinAvailableAmount : - (priceInfo.MaxAvailableAmount > 0 ? Math.Min(priceInfo.MaxAvailableAmount, 5) : 5); + int quantity = PriceInfo.DefaultAmount; + if (priceInfo.MaxAvailableAmount > 0) + { + if (priceInfo.MaxAvailableAmount > priceInfo.MinAvailableAmount) + { + quantity = Rand.Range(priceInfo.MinAvailableAmount, priceInfo.MaxAvailableAmount); + } + else + { + quantity = priceInfo.MaxAvailableAmount; + } + } + else if (priceInfo.MinAvailableAmount > 0) + { + quantity = priceInfo.MinAvailableAmount; + } stock.Add(new PurchasedItem(prefab, quantity)); } } return stock; } - public static List LoadStoreStock(XElement storeElement) - { - var stock = new List(); - if (storeElement == null) { return stock; } - foreach (XElement stockElement in storeElement.GetChildElements("stock")) - { - var id = stockElement.GetAttributeString("id", null); - if (string.IsNullOrWhiteSpace(id)) { continue; } - var prefab = ItemPrefab.Prefabs.Find(p => p.Identifier == id); - if (prefab == null) { continue; } - var qty = stockElement.GetAttributeInt("qty", 0); - if (qty < 1) { continue; } - stock.Add(new PurchasedItem(prefab, qty)); - } - return stock; - } - /// /// Mark the items that have been taken from the outpost to prevent them from spawning when re-entering the outpost /// @@ -526,48 +650,70 @@ namespace Barotrauma } } - public int GetAdjustedItemBuyPrice(PriceInfo priceInfo) + /// If null, item.GetPriceInfo() will be used to get it. + /// /// If false, the price won't be affected by + public int GetAdjustedItemBuyPrice(ItemPrefab item, PriceInfo priceInfo = null, bool considerDailySpecials = true) { - // TODO: Check priceInfo.CanBeBought + priceInfo ??= item?.GetPriceInfo(this); if (priceInfo == null) { return 0; } - var price = priceInfo.Price; + float price = priceInfo.Price; + + // Adjust by random price modifier + price = ((100 + StorePriceModifier) / 100.0f) * price; + + // Adjust by daily special status + if (considerDailySpecials && DailySpecials.Contains(item)) + { + price = DailySpecialPriceModifier * price; + } + + // Adjust by current location reputation if (Reputation.Value > 0.0f) { - price = (int)(MathHelper.Lerp(1.0f, 1.0f - StoreMaxReputationModifier, Reputation.Value / Reputation.MaxReputation) * price); + price = MathHelper.Lerp(1.0f, 1.0f - StoreMaxReputationModifier, Reputation.Value / Reputation.MaxReputation) * price; } else { - price = (int)(MathHelper.Lerp(1.0f, 1.0f + StoreMaxReputationModifier, Reputation.Value / Reputation.MinReputation) * price); + price = MathHelper.Lerp(1.0f, 1.0f + StoreMaxReputationModifier, Reputation.Value / Reputation.MinReputation) * price; } - // Item price should never go below 1 mk - return Math.Max(price, 1); + + // Price should never go below 1 mk + return Math.Max((int)price, 1); } - /// - /// If item.GetPriceInfo() returns null, this will return 0 - /// - public int GetAdjustedItemBuyPrice(ItemPrefab item) => GetAdjustedItemBuyPrice(item?.GetPriceInfo(this)); - - public int GetAdjustedItemSellPrice(PriceInfo priceInfo) + /// If null, item.GetPriceInfo() will be used to get it. + /// If false, the price won't be affected by + public int GetAdjustedItemSellPrice(ItemPrefab item, PriceInfo priceInfo = null, bool considerRequestedGoods = true) { + priceInfo ??= item?.GetPriceInfo(this); if (priceInfo == null) { return 0; } - var price = (int)(StoreSellPriceModifier * priceInfo.Price); + float price = StoreSellPriceModifier * priceInfo.Price; + + // Adjust by random price modifier + price = ((100 - StorePriceModifier) / 100.0f) * price; + + // Adjust by current store balance + price = ActiveStoreBalanceStatus.SellPriceModifier * price; + + // Adjust by requested good status + if (considerRequestedGoods && RequestedGoods.Contains(item)) + { + price = RequestGoodPriceModifier * price; + } + + // Adjust by current location reputation if (Reputation.Value > 0.0f) { - price = (int)(MathHelper.Lerp(1.0f, 1.0f + StoreMaxReputationModifier, Reputation.Value / Reputation.MaxReputation) * price); + price = MathHelper.Lerp(1.0f, 1.0f + StoreMaxReputationModifier, Reputation.Value / Reputation.MaxReputation) * price; } else { - price = (int)(MathHelper.Lerp(1.0f, 1.0f - StoreMaxReputationModifier, Reputation.Value / Reputation.MinReputation) * price); + price = MathHelper.Lerp(1.0f, 1.0f - StoreMaxReputationModifier, Reputation.Value / Reputation.MinReputation) * price; } - // Item price should never go below 1 mk - return Math.Max(price, 1); - } - /// - /// If item.GetPriceInfo() returns null, this will return 0 - /// - public int GetAdjustedItemSellPrice(ItemPrefab item) => GetAdjustedItemSellPrice(item?.GetPriceInfo(this)); + // Price should never go below 1 mk + return Math.Max((int)price, 1); + } public int GetAdjustedMechanicalCost(int cost) { @@ -575,12 +721,12 @@ namespace Barotrauma return (int) Math.Ceiling((1.0f - discount) * cost * MechanicalPriceMultiplier); } - /// - /// If 'force' is true, the stock will be recreated even if it has been created previously already. - /// This is used when (at least) when the type of the location changes. - /// + /// If true, the store will be recreated if it already exists. public void CreateStore(bool force = false) { + // In multiplayer, stores should be created by the server and loaded from save data by clients + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } + if (!force && StoreStock != null) { return; } if (StoreStock != null) @@ -604,10 +750,16 @@ namespace Barotrauma StoreCurrentBalance = StoreInitialBalance; StoreStock = CreateStoreStock(); } + + GenerateRandomPriceModifier(); + CreateStoreSpecials(); } public void UpdateStore() { + // In multiplayer, stores should be updated by the server and loaded from save data by clients + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } + if (StoreStock == null) { CreateStore(); @@ -619,6 +771,8 @@ namespace Barotrauma StoreCurrentBalance = Math.Min(StoreCurrentBalance + (int)(StoreInitialBalance / 10.0f), StoreInitialBalance); } + GenerateRandomPriceModifier(); + var stock = StoreStock; var stockToRemove = new List(); foreach (PurchasedItem item in stock) @@ -642,6 +796,56 @@ namespace Barotrauma } stockToRemove.ForEach(i => stock.Remove(i)); StoreStock = stock; + + if (++StepsSinceSpecialsUpdated >= SpecialsUpdateInterval) + { + CreateStoreSpecials(); + } + } + + private void GenerateRandomPriceModifier() + { + StorePriceModifier = Rand.Range(-StorePriceModifierRange, StorePriceModifierRange); + } + + private void CreateStoreSpecials() + { + DailySpecials.Clear(); + var availableStock = new Dictionary(); + foreach (var stockItem in StoreStock) + { + if (stockItem.Quantity < 1) { continue; } + var weight = 1.0f; + var priceInfo = stockItem.ItemPrefab.GetPriceInfo(this); + if (priceInfo != null) + { + if (!priceInfo.CanBeSpecial) { continue; } + var baseQuantity = priceInfo.MinAvailableAmount > 0 ? priceInfo.MinAvailableAmount : PriceInfo.DefaultAmount; + weight += (float)(stockItem.Quantity - baseQuantity) / baseQuantity; + if (weight < 0.0f) { continue; } + } + availableStock.Add(stockItem.ItemPrefab, weight); + } + for (int i = 0; i < DailySpecialsCount; i++) + { + if (availableStock.None()) { break; } + var item = ToolBox.SelectWeightedRandom(availableStock.Keys.ToList(), availableStock.Values.ToList(), Rand.RandSync.Unsynced); + if (item == null) { break; } + DailySpecials.Add(item); + availableStock.Remove(item); + } + + RequestedGoods.Clear(); + for (int i = 0; i < RequestedGoodsCount; i++) + { + var item = ItemPrefab.Prefabs.GetRandom(p => + p.CanBeSold && !RequestedGoods.Contains(p) && + p.GetPriceInfo(this) is PriceInfo pi && pi.CanBeSpecial); + if (item == null) { break; } + RequestedGoods.Add(item); + } + + StepsSinceSpecialsUpdated = 0; } public void AddToStock(List items) @@ -686,6 +890,20 @@ namespace Barotrauma } } + public static StoreBalanceStatus GetStoreBalanceStatus(int balance) + { + StoreBalanceStatus nextStatus = DefaultBalanceStatus; + foreach (var balanceStatus in StoreBalanceStatuses) + { + if (balanceStatus.PercentageOfInitialBalance < nextStatus.PercentageOfInitialBalance && + ((float)balance / StoreInitialBalance) < balanceStatus.PercentageOfInitialBalance) + { + nextStatus = balanceStatus; + } + } + return nextStatus; + } + public XElement Save(Map map, XElement parentElement) { var locationElement = new XElement("location", @@ -695,13 +913,24 @@ namespace Barotrauma new XAttribute("discovered", Discovered), new XAttribute("position", XMLExtensions.Vector2ToString(MapPosition)), new XAttribute("pricemultiplier", PriceMultiplier), - new XAttribute("mechanicalpricemultipler", MechanicalPriceMultiplier)); + new XAttribute("mechanicalpricemultipler", MechanicalPriceMultiplier), + new XAttribute("timesincelasttypechange", TimeSinceLastTypeChange)); LevelData.Save(locationElement); - if (TypeChangeTimer > 0) + for (int i = 0; i < Type.CanChangeTo.Count; i++) { - locationElement.Add(new XAttribute("changetimer", TypeChangeTimer)); + if (ProximityTimer.ContainsKey(Type.CanChangeTo[i])) + { + locationElement.Add(new XAttribute("proximitytimer" + i, ProximityTimer[Type.CanChangeTo[i]])); + } } + + if (PendingLocationTypeChange != null) + { + locationElement.Add(new XAttribute("pendinglocationtypechange", Type.CanChangeTo.IndexOf(PendingLocationTypeChange.First))); + locationElement.Add(new XAttribute("pendinglocationtypechangetimer", PendingLocationTypeChange.Second)); + } + if (takenItems.Any()) { locationElement.Add(new XAttribute( @@ -715,7 +944,11 @@ namespace Barotrauma if (StoreStock != null) { - var storeElement = new XElement("store", new XAttribute("balance", StoreCurrentBalance)); + var storeElement = new XElement("store", + new XAttribute("balance", StoreCurrentBalance), + new XAttribute("pricemodifier", StorePriceModifier), + new XAttribute("stepssincespecialsupdated", StepsSinceSpecialsUpdated)); + foreach (PurchasedItem item in StoreStock) { if (item?.ItemPrefab == null) { continue; } @@ -723,6 +956,29 @@ namespace Barotrauma new XAttribute("id", item.ItemPrefab.Identifier), new XAttribute("qty", item.Quantity))); } + + if (DailySpecials.Any()) + { + var dailySpecialElement = new XElement("dailyspecials"); + foreach (var item in DailySpecials) + { + dailySpecialElement.Add(new XElement("item", + new XAttribute("id", item.Identifier))); + } + storeElement.Add(dailySpecialElement); + } + + if (RequestedGoods.Any()) + { + var requestedGoodsElement = new XElement("requestedgoods"); + foreach (var item in RequestedGoods) + { + requestedGoodsElement.Add(new XElement("item", + new XAttribute("id", item.Identifier))); + } + storeElement.Add(requestedGoodsElement); + } + locationElement.Add(storeElement); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationConnection.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationConnection.cs index d2c0cecd3..71234355e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationConnection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationConnection.cs @@ -1,4 +1,5 @@ using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; namespace Barotrauma @@ -33,6 +34,19 @@ namespace Barotrauma public LocationConnection(Location location1, Location location2) { + if (location1 == null) + { + throw new ArgumentException("Invalid location connection: location1 was null"); + } + if (location2 == null) + { + throw new ArgumentException("Invalid location connection: location2 was null"); + } + if (location1 == location2) + { + throw new ArgumentException("Invalid location connection: location1 was the same as location2"); + } + Locations = new Location[] { location1, location2 }; Length = Vector2.Distance(location1.MapPosition, location2.MapPosition); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs index fc569425f..2353a7b8d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs @@ -1,4 +1,5 @@ -using System; +using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; @@ -9,35 +10,85 @@ namespace Barotrauma { public readonly string ChangeToType; - public readonly float Probability; - public readonly int RequiredDuration; - - public readonly float ProximityProbabilityIncrease; - public readonly int RequiredProximityForProbabilityIncrease; - public readonly bool RequireDiscovered; public List Messages = new List(); - //the change can't happen if there's a location of the given type next to this one + /// + /// The change can only happen if there's at least one of the given types of locations near this one + /// + public readonly List RequiredLocations; + + /// + /// How close the location needs to be to one of the RequiredLocations for the change to occur + /// + public readonly int RequiredProximity; + + /// + /// Base probability per turn for the location to change if near one of the RequiredLocations + /// + public readonly float Probability; + + /// + /// How close the location needs to be to one of the RequiredLocations for the probability to increase + /// + public readonly int RequiredProximityForProbabilityIncrease; + + /// + /// How much the probability increases per turn if within RequiredProximityForProbabilityIncrease steps of RequiredLocations + /// + public readonly float ProximityProbabilityIncrease; + + /// + /// The change can't happen if there's one or more of the given types of locations near this one + /// public readonly List DisallowedAdjacentLocations; - //the change can only happen if there's at least one of the given types of locations next to this one - public readonly List RequiredAdjacentLocations; + /// + /// How close the location needs to be to one of the DisallowedAdjacentLocations for the change to be disabled + /// + public readonly int DisallowedProximity; + + public readonly Point RequiredDurationRange; public LocationTypeChange(string currentType, XElement element) { ChangeToType = element.GetAttributeString("type", ""); Probability = element.GetAttributeFloat("probability", 1.0f); - RequiredDuration = element.GetAttributeInt("requiredduration", 0); RequireDiscovered = element.GetAttributeBool("requirediscovered", false); + RequiredLocations = element.GetAttributeStringArray("requiredlocations", element.GetAttributeStringArray("requiredadjacentlocations", new string[0])).ToList(); + RequiredProximity = Math.Max(element.GetAttributeInt("requiredproximity", 1), 1); ProximityProbabilityIncrease = element.GetAttributeFloat("proximityprobabilityincrease", 0.0f); RequiredProximityForProbabilityIncrease = element.GetAttributeInt("requiredproximityforprobabilityincrease", -1); + + if (RequiredProximityForProbabilityIncrease > 0 || ProximityProbabilityIncrease > 0.0f) + { + if (!RequiredLocations.Any()) + { + DebugConsole.AddWarning( + $"Invalid location type change in location type \"{currentType}\". "+ + "Probability is configured to increase when near some other type of location, but the RequiredLocations attribute is not set."); + } + if (Probability >= 1.0f) + { + DebugConsole.AddWarning( + $"Invalid location type change in location type \"{currentType}\". " + + "Probability is configured to increase when near some other type of location, but the base probability is already 100%"); + } + } + DisallowedAdjacentLocations = element.GetAttributeStringArray("disallowedadjacentlocations", new string[0]).ToList(); - RequiredAdjacentLocations = element.GetAttributeStringArray("requiredadjacentlocations", new string[0]).ToList(); + DisallowedProximity = Math.Max(element.GetAttributeInt("disallowedproximity", 1), 1); + + RequiredDurationRange = element.GetAttributePoint("requireddurationrange", Point.Zero); + //backwards compatibility + if (element.Attribute("requiredduration") != null) + { + RequiredDurationRange = new Point(element.GetAttributeInt("requiredduration", 0)); + } string messageTag = element.GetAttributeString("messagetag", "LocationChange." + currentType + ".ChangeTo." + ChangeToType); @@ -50,15 +101,31 @@ namespace Barotrauma public float DetermineProbability(Location location) { - float totalProbability = Probability; - if (AnyWithinRequiredProximity(location)) { totalProbability += ProximityProbabilityIncrease; } - return totalProbability; + if (RequireDiscovered && !location.Discovered) { return 0.0f; } + + if (RequiredLocations.Any() && !AnyWithinDistance(location, RequiredProximity, (otherLocation) => { return RequiredLocations.Contains(otherLocation.Type.Identifier); })) + { + return 0.0f; + } + if (DisallowedAdjacentLocations.Any() && AnyWithinDistance(location, DisallowedProximity, (otherLocation) => { return DisallowedAdjacentLocations.Contains(otherLocation.Type.Identifier); })) + { + return 0.0f; + } + float probability = Probability; + if (location.ProximityTimer.ContainsKey(this)) + { + if (AnyWithinDistance(location, RequiredProximityForProbabilityIncrease, (otherLocation) => { return RequiredLocations.Contains(otherLocation.Type.Identifier); })) + { + return probability += ProximityProbabilityIncrease * location.ProximityTimer[this]; + } + } + return probability; } - private bool AnyWithinRequiredProximity(Location location, int currentDistance = 0, HashSet checkedLocations = null) + public bool AnyWithinDistance(Location location, int maxDistance, Func predicate, int currentDistance = 0, HashSet checkedLocations = null) { - if (currentDistance > RequiredProximityForProbabilityIncrease) { return false; } - if (currentDistance > 0 && RequiredAdjacentLocations.Contains(location.Type.Identifier)) { return true; } + if (currentDistance > maxDistance) { return false; } + if (currentDistance > 0 && predicate(location)) { return true; } checkedLocations ??= new HashSet(); checkedLocations.Add(location); @@ -68,7 +135,7 @@ namespace Barotrauma var otherLocation = connection.OtherLocation(location); if (!checkedLocations.Contains(otherLocation)) { - if (AnyWithinRequiredProximity(otherLocation, currentDistance + 1, checkedLocations)) { return true; } + if (AnyWithinDistance(otherLocation, maxDistance, predicate, currentDistance + 1, checkedLocations)) { return true; } } } @@ -78,7 +145,7 @@ namespace Barotrauma private int CountWithinRequiredProximity(Location location, int currentDistance = 0, HashSet checkedLocations = null) { if (currentDistance > RequiredProximityForProbabilityIncrease) { return 0; } - int count = currentDistance > 0 && RequiredAdjacentLocations.Contains(location.Type.Identifier) ? 1 : 0; + int count = currentDistance > 0 && RequiredLocations.Contains(location.Type.Identifier) ? 1 : 0; checkedLocations ??= new HashSet(); checkedLocations.Add(location); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index aff3e4fc1..1fd5e93de 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -96,6 +96,7 @@ namespace Barotrauma { case "connection": Point locationIndices = subElement.GetAttributePoint("locations", new Point(0, 1)); + if (locationIndices.X == locationIndices.Y) { continue; } var connection = new LocationConnection(Locations[locationIndices.X], Locations[locationIndices.Y]) { Passed = subElement.GetAttributeBool("passed", false), @@ -185,8 +186,8 @@ namespace Barotrauma } System.Diagnostics.Debug.Assert(StartLocation != null, "Start location not assigned after level generation."); - CurrentLocation.CreateStore(); CurrentLocation.Discovered = true; + CurrentLocation.CreateStore(); InitProjectSpecific(); } @@ -247,7 +248,7 @@ namespace Barotrauma for (int i = 0; i < 2; i++) { - if (newLocations[i] != null) continue; + if (newLocations[i] != null) { continue; } Vector2[] points = new Vector2[] { edge.Point1, edge.Point2 }; @@ -320,7 +321,15 @@ namespace Barotrauma { connection.Locations[1] = Locations[i]; } - Locations[i].Connections.Add(connection); + + if (connection.Locations[0] != connection.Locations[1]) + { + Locations[i].Connections.Add(connection); + } + else + { + Connections.Remove(connection); + } } Locations[i].Connections.RemoveAll(c => c.OtherLocation(Locations[i]) == Locations[j]); Locations.RemoveAt(j); @@ -548,6 +557,12 @@ namespace Barotrauma CurrentLocation.CreateStore(); OnLocationChanged?.Invoke(prevLocation, CurrentLocation); + + if (GameMain.GameSession?.GameMode is CampaignMode campaign && campaign.CampaignMetadata is { } metadata) + { + metadata.SetValue("campaign.location.id", CurrentLocationIndex); + metadata.SetValue("campaign.location.name", CurrentLocation.Name); + } } public void SetLocation(int index) @@ -678,92 +693,105 @@ namespace Barotrauma { foreach (Location location in Locations) { - if (furthestDiscoveredLocation == null || - location.MapPosition.X > furthestDiscoveredLocation.MapPosition.X) + if (location.Discovered) { - furthestDiscoveredLocation = location; + if (furthestDiscoveredLocation == null || + location.MapPosition.X > furthestDiscoveredLocation.MapPosition.X) + { + furthestDiscoveredLocation = location; + } } } foreach (Location location in Locations) { - if (location.MapPosition.X < furthestDiscoveredLocation.MapPosition.X) + if (location.MapPosition.X > furthestDiscoveredLocation.MapPosition.X) { - furthestDiscoveredLocation = location; + continue; } if (location == CurrentLocation || location == SelectedLocation) { continue; } - //find which types of locations this one can change to - var cct = location.Type.CanChangeTo; - List allowedTypeChanges = new List(); - List readyTypeChanges = new List(); - for (int i = 0; i < cct.Count; i++) + ProgressLocationTypeChanges(location); + + if (location.Discovered) { - LocationTypeChange typeChange = cct[i]; - if (typeChange.RequireDiscovered && !location.Discovered) { continue; } - //check if there are any adjacent locations that would prevent the change - bool disallowedFound = false; - foreach (string disallowedLocationName in typeChange.DisallowedAdjacentLocations) - { - if (location.Connections.Any(c => c.OtherLocation(location).Type.Identifier.Equals(disallowedLocationName, StringComparison.OrdinalIgnoreCase))) - { - disallowedFound = true; - break; - } - } - if (disallowedFound) { continue; } - - //check that there's a required adjacent location present - bool requiredFound = false; - foreach (string requiredLocationName in typeChange.RequiredAdjacentLocations) - { - if (location.Connections.Any(c => c.OtherLocation(location).Type.Identifier.Equals(requiredLocationName, StringComparison.OrdinalIgnoreCase))) - { - requiredFound = true; - break; - } - } - if (!requiredFound && typeChange.RequiredAdjacentLocations.Count > 0) { continue; } - - allowedTypeChanges.Add(typeChange); - - if (location.TypeChangeTimer >= typeChange.RequiredDuration) - { - readyTypeChanges.Add(i); - } + location.UpdateStore(); } + } + } - List readyTypeProbabilities = readyTypeChanges.Select(i => cct[i].DetermineProbability(location)).ToList(); - //select a random type change - if (Rand.Range(0.0f, 1.0f) < readyTypeChanges.Sum(i => readyTypeProbabilities[i])) + private void ProgressLocationTypeChanges(Location location) + { + location.TimeSinceLastTypeChange++; + + if (location.PendingLocationTypeChange != null) + { + if (location.PendingLocationTypeChange.First.DetermineProbability(location) <= 0.0f) { - var selectedTypeChangeIndex = - ToolBox.SelectWeightedRandom( - readyTypeChanges, - readyTypeChanges.Select(i => readyTypeProbabilities[i]).ToList(), - Rand.RandSync.Unsynced); - var selectedTypeChange = cct[selectedTypeChangeIndex]; - if (selectedTypeChange != null) - { - string prevName = location.Name; - location.ChangeType(LocationType.List.Find(lt => lt.Identifier.Equals(selectedTypeChange.ChangeToType, StringComparison.OrdinalIgnoreCase))); - ChangeLocationType(location, prevName, selectedTypeChange); - location.TypeChangeTimer = -1; - } - } - - if (allowedTypeChanges.Count > 0) - { - location.TypeChangeTimer++; + //remove pending type change if it's no longer allowed + location.PendingLocationTypeChange = null; } else { - location.TypeChangeTimer = 0; + location.PendingLocationTypeChange.Second--; + if (location.PendingLocationTypeChange.Second <= 0) + { + ChangeLocationType(location, location.PendingLocationTypeChange.First); + } + return; } - - location.UpdateStore(); } + + //find which types of locations this one can change to + Dictionary allowedTypeChanges = new Dictionary(); + foreach (LocationTypeChange typeChange in location.Type.CanChangeTo) + { + float probability = typeChange.DetermineProbability(location); + if (probability <= 0.0f) { continue; } + allowedTypeChanges.Add(typeChange, probability); + } + + //select a random type change + if (Rand.Range(0.0f, 1.0f) < allowedTypeChanges.Sum(change => change.Value)) + { + var selectedTypeChange = + ToolBox.SelectWeightedRandom( + allowedTypeChanges.Keys.ToList(), + allowedTypeChanges.Values.ToList(), + Rand.RandSync.Unsynced); + if (selectedTypeChange != null) + { + if (selectedTypeChange.RequiredDurationRange.X > 0) + { + location.PendingLocationTypeChange = new Pair( + selectedTypeChange, + Rand.Range(selectedTypeChange.RequiredDurationRange.X, selectedTypeChange.RequiredDurationRange.Y)); + } + else + { + ChangeLocationType(location, selectedTypeChange); + } + return; + } + } + + foreach (LocationTypeChange typeChange in location.Type.CanChangeTo) + { + if (typeChange.AnyWithinDistance( + location, + typeChange.RequiredProximityForProbabilityIncrease, + (otherLocation) => { return typeChange.RequiredLocations.Contains(otherLocation.Type.Identifier); })) + { + if (!location.ProximityTimer.ContainsKey(typeChange)) { location.ProximityTimer[typeChange] = 0; } + location.ProximityTimer[typeChange] += 1; + } + else + { + location.ProximityTimer.Remove(typeChange); + } + } + } public int DistanceToClosestLocationWithOutpost(Location startingLocation, out Location endingLocation) @@ -811,7 +839,18 @@ namespace Barotrauma return distance; } - partial void ChangeLocationType(Location location, string prevName, LocationTypeChange change); + private void ChangeLocationType(Location location, LocationTypeChange change) + { + string prevName = location.Name; + location.ChangeType(LocationType.List.Find(lt => lt.Identifier.Equals(change.ChangeToType, StringComparison.OrdinalIgnoreCase))); + ChangeLocationTypeProjSpecific(location, prevName, change); + location.ProximityTimer.Remove(change); + location.TimeSinceLastTypeChange = 0; + location.PendingLocationTypeChange = null; + } + + partial void ChangeLocationTypeProjSpecific(Location location, string prevName, LocationTypeChange change); + partial void ClearAnimQueue(); /// @@ -847,8 +886,19 @@ namespace Barotrauma { case "location": Location location = Locations[subElement.GetAttributeInt("i", 0)]; - - location.TypeChangeTimer = subElement.GetAttributeInt("changetimer", 0); + location.ProximityTimer.Clear(); + for (int i = 0; i < location.Type.CanChangeTo.Count; i++) + { + location.ProximityTimer.Add(location.Type.CanChangeTo[i], subElement.GetAttributeInt("changetimer" + i, 0)); + } + int locationTypeChangeIndex = subElement.GetAttributeInt("pendinglocationtypechange", -1); + if (locationTypeChangeIndex > 0 && locationTypeChangeIndex < location.Type.CanChangeTo.Count - 1) + { + location.PendingLocationTypeChange = new Pair( + location.Type.CanChangeTo[locationTypeChangeIndex], + subElement.GetAttributeInt("pendinglocationtypechangetimer", 0)); + } + location.TimeSinceLastTypeChange = subElement.GetAttributeInt("timesincelasttypechange", 0); location.Discovered = subElement.GetAttributeBool("discovered", false); if (location.Discovered) { @@ -861,7 +911,6 @@ namespace Barotrauma } } - string locationType = subElement.GetAttributeString("type", ""); string prevLocationName = location.Name; LocationType prevLocationType = location.Type; @@ -872,10 +921,14 @@ namespace Barotrauma var change = prevLocationType.CanChangeTo.Find(c => c.ChangeToType.Equals(location.Type.Identifier, StringComparison.OrdinalIgnoreCase)); if (change != null) { - ChangeLocationType(location, prevLocationName, change); + ChangeLocationTypeProjSpecific(location, prevLocationName, change); + location.TimeSinceLastTypeChange = 0; } } + + location.LoadStore(subElement); location.LoadMissions(subElement); + break; case "connection": int connectionIndex = subElement.GetAttributeInt("i", 0); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs index a70672a87..873ab9a84 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -249,10 +249,6 @@ namespace Barotrauma { get { return ""; } } - - private bool ignoreByAI; - public bool IgnoreByAI => ignoreByAI; - public void SetIgnoreByAI(bool ignore) => ignoreByAI = ignore; public MapEntity(MapEntityPrefab prefab, Submarine submarine, ushort id) : base(submarine, id) { @@ -624,6 +620,21 @@ namespace Barotrauma continue; } + if (t == typeof(Structure)) + { + string name = element.Attribute("name").Value; + string identifier = element.GetAttributeString("identifier", ""); + StructurePrefab structurePrefab = Structure.FindPrefab(name, identifier); + if (structurePrefab == null) + { + ItemPrefab itemPrefab = ItemPrefab.Find(name, identifier); + if (itemPrefab != null) + { + t = typeof(Item); + } + } + } + try { MethodInfo loadMethod = t.GetMethod("Load", new[] { typeof(XElement), typeof(Submarine), typeof(IdRemap) }); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index 998ad3cd3..0ddc1b730 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs @@ -1386,10 +1386,10 @@ namespace Barotrauma { gotoTarget = outpost.GetHulls(true).GetRandom(); } - characterInfo.TeamID = Character.TeamType.FriendlyNPC; + characterInfo.TeamID = CharacterTeamType.FriendlyNPC; 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 = Character.TeamType.FriendlyNPC; + npc.TeamID = CharacterTeamType.FriendlyNPC; if (!outpost.Info.OutpostNPCs.ContainsKey(humanPrefab.Identifier)) { outpost.Info.OutpostNPCs.Add(humanPrefab.Identifier, new List()); @@ -1404,9 +1404,9 @@ namespace Barotrauma npc.CharacterHealth.MaxVitality *= humanPrefab.HealthMultiplier; } humanPrefab.GiveItems(npc, outpost, Rand.RandSync.Server); - foreach (Item item in npc.Inventory.Items) + foreach (Item item in npc.Inventory.FindAllItems(it => it != null, recursive: true)) { - if (item != null) { item.SpawnedInOutpost = true; } + item.SpawnedInOutpost = true; } npc.GiveIdCardTags(gotoTarget as WayPoint); if (npc.AIController is HumanAIController humanAI) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs index e0cbb2de9..e9eea6444 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Xml.Linq; namespace Barotrauma @@ -13,6 +12,14 @@ namespace Barotrauma public readonly int MinAvailableAmount; //maximum number of items available at a given store public readonly int MaxAvailableAmount; + /// + /// Used when both and are set to 0. + /// + public const int DefaultAmount = 5; + /// + /// Can the item be a Daily Special or a Requested Good + /// + public readonly bool CanBeSpecial; /// /// Support for the old style of determining item prices @@ -23,16 +30,21 @@ namespace Barotrauma { Price = element.GetAttributeInt("buyprice", 0); CanBeBought = true; - MinAvailableAmount = GetMinAmount(element); - MaxAvailableAmount = GetMaxAmount(element); + var minAmount = GetMinAmount(element); + MinAvailableAmount = Math.Min(minAmount, CargoManager.MaxQuantity); + var maxAmount = GetMaxAmount(element); + maxAmount = Math.Min(maxAmount, CargoManager.MaxQuantity); + MaxAvailableAmount = Math.Max(maxAmount, MinAvailableAmount); } - public PriceInfo(int price, bool canBeBought, int minAmount = 0, int maxAmount = 0) + public PriceInfo(int price, bool canBeBought, int minAmount = 0, int maxAmount = 0, bool canBeSpecial = true) { Price = price; CanBeBought = canBeBought; - MinAvailableAmount = minAmount; - MaxAvailableAmount = maxAmount; + MinAvailableAmount = Math.Min(minAmount, CargoManager.MaxQuantity); + maxAmount = Math.Min(maxAmount, CargoManager.MaxQuantity); + MaxAvailableAmount = Math.Max(maxAmount, minAmount); + CanBeSpecial = canBeSpecial; } public static List> CreatePriceInfos(XElement element, out PriceInfo defaultPrice) @@ -42,6 +54,7 @@ namespace Barotrauma var soldByDefault = element.GetAttributeBool("soldbydefault", true); var minAmount = GetMinAmount(element); var maxAmount = GetMaxAmount(element); + var canBeSpecial = element.GetAttributeBool("canbespecial", true); var priceInfos = new List>(); foreach (XElement childElement in element.GetChildElements("price")) @@ -51,13 +64,15 @@ namespace Barotrauma priceInfos.Add(new Tuple(childElement.GetAttributeString("locationtype", "").ToLowerInvariant(), new PriceInfo(price: (int)(priceMultiplier * basePrice), canBeBought: sold, minAmount: sold ? GetMinAmount(childElement, minAmount) : 0, - maxAmount: sold ? GetMaxAmount(childElement, maxAmount) : 0))); + maxAmount: sold ? GetMaxAmount(childElement, maxAmount) : 0, + canBeSpecial: canBeSpecial))); } var canBeBoughtAtOtherLocations = soldByDefault && element.GetAttributeBool("soldeverywhere", true); defaultPrice = new PriceInfo(basePrice, canBeBoughtAtOtherLocations, minAmount: canBeBoughtAtOtherLocations ? minAmount : 0, - maxAmount: canBeBoughtAtOtherLocations ? maxAmount : 0); + maxAmount: canBeBoughtAtOtherLocations ? maxAmount : 0, + canBeSpecial: canBeSpecial); return priceInfos; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index 426a8ad04..f027e8008 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -14,12 +14,11 @@ using Microsoft.Xna.Framework.Graphics; namespace Barotrauma { - partial class WallSection : ISpatialEntity + partial class WallSection : IIgnorable { public Rectangle rect; public float damage; public Gap gap; - private bool ignoreByAI; public Structure Wall { get; } public Vector2 Position => Wall.SectionPosition(Wall.Sections.IndexOf(this)); @@ -28,7 +27,8 @@ 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 => ignoreByAI; + public bool IgnoreByAI => OrderedToBeIgnored; + public bool OrderedToBeIgnored { get; set; } public WallSection(Rectangle rect, Structure wall, float damage = 0.0f) { @@ -37,8 +37,6 @@ namespace Barotrauma this.damage = damage; Wall = wall; } - - public void SetIgnoreByAI(bool ignore) => ignoreByAI = ignore; } partial class Structure : MapEntity, IDamageable, IServerSerializable, ISerializableEntity @@ -843,7 +841,7 @@ namespace Barotrauma public int FindSectionIndex(Vector2 displayPos, bool world = false, bool clamp = false) { - if (!Sections.Any()) return -1; + if (Sections.None()) { return -1; } if (world && Submarine != null) { @@ -857,7 +855,7 @@ namespace Barotrauma displayPos.X += WallSectionSize - Sections[0].rect.Width; } - int index = (IsHorizontal) ? + int index = IsHorizontal ? (int)Math.Floor((displayPos.X - rect.X) / WallSectionSize) : (int)Math.Floor((rect.Y - displayPos.Y) / WallSectionSize); @@ -1082,6 +1080,7 @@ namespace Barotrauma if (attacker != null && damageDiff != 0.0f) { + HumanAIController.StructureDamaged(this, damageDiff, attacker); OnHealthChangedProjSpecific(attacker, damageDiff); if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) { @@ -1089,7 +1088,7 @@ namespace Barotrauma { attacker.Info.IncreaseSkillLevel("mechanical", -damageDiff * SkillSettings.Current.SkillIncreasePerRepairedStructureDamage / Math.Max(attacker.GetSkillLevel("mechanical"), 1.0f), - SectionPosition(sectionIndex, true)); + SectionPosition(sectionIndex)); } } } @@ -1301,6 +1300,7 @@ namespace Barotrauma SerializableProperty.UpgradeGameVersion(s, s.Prefab.ConfigElement, submarine.Info.GameVersion); } + bool hasDamage = false; foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -1317,7 +1317,9 @@ namespace Barotrauma } else { - s.Sections[index].damage = subElement.GetAttributeFloat("damage", 0.0f); + float damage = subElement.GetAttributeFloat("damage", 0.0f); + s.Sections[index].damage = damage; + hasDamage |= damage > 0.0f; } break; case "upgrade": @@ -1339,8 +1341,8 @@ namespace Barotrauma } } - if (element.GetAttributeBool("flippedx", false)) s.FlipX(false); - if (element.GetAttributeBool("flippedy", false)) s.FlipY(false); + if (element.GetAttributeBool("flippedx", false)) { s.FlipX(false); } + if (element.GetAttributeBool("flippedy", false)) { s.FlipY(false); } //structures with a body drop a shadow by default if (element.Attribute("usedropshadow") == null) @@ -1353,6 +1355,11 @@ namespace Barotrauma s.NoAITarget = prefab.NoAITarget; } + if (hasDamage) + { + s.UpdateSections(); + } + return s; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index 71dd3e156..783bf3730 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -25,7 +25,7 @@ namespace Barotrauma { public SubmarineInfo Info { get; private set; } - public Character.TeamType TeamID = Character.TeamType.None; + public CharacterTeamType TeamID = CharacterTeamType.None; public static readonly Vector2 HiddenSubStartPosition = new Vector2(-50000.0f, 10000.0f); //position of the "actual submarine" which is rendered wherever the SubmarineBody is @@ -254,7 +254,7 @@ namespace Barotrauma get { if (Level.Loaded == null || subBody == null) { return false; } - return RealWorldDepth > Level.Loaded.RealWorldCrushDepth; + return RealWorldDepth > Level.Loaded.RealWorldCrushDepth & RealWorldDepth > RealWorldCrushDepth; } } @@ -325,7 +325,7 @@ namespace Barotrauma Info.Type = SubmarineType.Wreck; ShowSonarMarker = false; PhysicsBody.FarseerBody.BodyType = BodyType.Static; - TeamID = Character.TeamType.None; + TeamID = CharacterTeamType.None; string defaultTag = Level.Loaded.GetWreckIDTag("wreck_id", this); ReplaceIDCardTagRequirements("wreck_id", defaultTag); @@ -554,7 +554,7 @@ namespace Barotrauma spawnPos.X = (limits.X + limits.Y) / 2 + subDockingPortOffset; } - spawnPos.Y = MathHelper.Clamp(spawnPos.Y, dockedBorders.Height / 2 + 10, Level.Loaded.Size.Y - dockedBorders.Height / 2 - 10); + spawnPos.Y = MathHelper.Clamp(spawnPos.Y, dockedBorders.Height / 2 + 10, Level.Loaded.Size.Y - dockedBorders.Height / 2 - padding * 2); return spawnPos - diffFromDockedBorders; } @@ -1332,7 +1332,7 @@ namespace Barotrauma { ShowSonarMarker = false; PhysicsBody.FarseerBody.BodyType = BodyType.Static; - TeamID = Character.TeamType.FriendlyNPC; + TeamID = CharacterTeamType.FriendlyNPC; foreach (MapEntity me in MapEntity.mapEntityList) { @@ -1495,7 +1495,19 @@ namespace Barotrauma if (e is Item item) { if (item.FindParentInventory(inv => inv is CharacterInventory) != null) { continue; } +#if CLIENT + if (Screen.Selected != GameMain.SubEditorScreen) + { + if (e.Submarine != this && item.GetRootContainer()?.Submarine != this) { continue; } + } + else + { + e.Submarine = this; + } +#else if (e.Submarine != this && item.GetRootContainer()?.Submarine != this) { continue; } +#endif + } else { @@ -1517,6 +1529,10 @@ namespace Barotrauma OutpostModuleInfo = Info.OutpostModuleInfo != null ? new OutpostModuleInfo(Info.OutpostModuleInfo) : null, Name = Path.GetFileNameWithoutExtension(filePath) }; +#if CLIENT + //remove reference to the preview image from the old info, so we don't dispose it (the new info still uses the texture) + Info.PreviewImage = null; +#endif Info.Dispose(); Info = newInfo; return newInfo.SaveAs(filePath, previewImage); } @@ -1691,6 +1707,7 @@ namespace Barotrauma } } } + node.Waypoint.FindHull(); } } @@ -1705,6 +1722,7 @@ namespace Barotrauma nodes.Clear(); obstructedNodes.Remove(otherSub); } + OutdoorNodes.ForEach(n => n.Waypoint.FindHull()); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs index 149fa1fad..19bf9df68 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs @@ -450,16 +450,19 @@ namespace Barotrauma #endif if (Level.Loaded == null) { return; } float submarineDepth = submarine.RealWorldDepth; - if (submarineDepth < Level.Loaded.RealWorldCrushDepth) { return; } + if (!Submarine.AtDamageDepth) { return; } depthDamageTimer -= deltaTime; if (depthDamageTimer > 0.0f) { return; } foreach (Structure wall in Structure.WallList) { - if (wall.Submarine != submarine || wall.CrushDepth > submarineDepth) { continue; } + if (wall.Submarine != submarine) { continue; } - float pastCrushDepth = submarineDepth - wall.CrushDepth; + float wallCrushDepth = wall.CrushDepth; + if (submarine.Info.SubmarineClass == SubmarineClass.DeepDiver) { wallCrushDepth *= 1.2f; } + float pastCrushDepth = submarine.RealWorldDepth - wallCrushDepth; + if (pastCrushDepth < 0) { return; } Explosion.RangedStructureDamage(wall.WorldPosition, 100.0f, pastCrushDepth * 0.1f, levelWallDamage: 0.0f); if (Character.Controlled != null && Character.Controlled.Submarine == submarine) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs index 783ee7440..3c056fbbc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs @@ -108,8 +108,11 @@ namespace Barotrauma { if (hash == null) { - XDocument doc = OpenFile(FilePath); - StartHashDocTask(doc); + if (hashTask == null) + { + XDocument doc = OpenFile(FilePath); + StartHashDocTask(doc); + } hashTask.Wait(); hashTask = null; } @@ -118,6 +121,11 @@ namespace Barotrauma } } + public bool CalculatingHash + { + get { return hashTask != null && !hashTask.IsCompleted; } + } + public Vector2 Dimensions { get; @@ -373,6 +381,10 @@ namespace Barotrauma public void Dispose() { +#if CLIENT + PreviewImage?.Remove(); + PreviewImage = null; +#endif if (savedSubmarines.Contains(this)) { savedSubmarines.Remove(this); } } @@ -522,12 +534,13 @@ namespace Barotrauma for (int i = savedSubmarines.Count - 1; i >= 0; i--) { - if (File.Exists(savedSubmarines[i].FilePath) && - savedSubmarines[i].LastModifiedTime == File.GetLastWriteTime(savedSubmarines[i].FilePath) && - (Path.GetFullPath(Path.GetDirectoryName(savedSubmarines[i].FilePath)) == Path.GetFullPath(SavePath) || - contentPackageSubs.Any(fp => Path.GetFullPath(fp.Path).CleanUpPath() == Path.GetFullPath(savedSubmarines[i].FilePath).CleanUpPath()))) + if (File.Exists(savedSubmarines[i].FilePath)) { - continue; + bool isDownloadedSub = Path.GetFullPath(Path.GetDirectoryName(savedSubmarines[i].FilePath)) == Path.GetFullPath(SaveUtil.SubmarineDownloadFolder); + bool isInSubmarinesFolder = Path.GetFullPath(Path.GetDirectoryName(savedSubmarines[i].FilePath)) == Path.GetFullPath(SavePath); + bool isInContentPackage = contentPackageSubs.Any(fp => Path.GetFullPath(fp.Path).CleanUpPath() == Path.GetFullPath(savedSubmarines[i].FilePath).CleanUpPath()); + if (isDownloadedSub) { continue; } + if (savedSubmarines[i].LastModifiedTime == File.GetLastWriteTime(savedSubmarines[i].FilePath) && (isInSubmarinesFolder || isInContentPackage)) { continue; } } savedSubmarines[i].Dispose(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs index ab9077d2e..633aec372 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs @@ -18,7 +18,7 @@ namespace Barotrauma public static bool ShowWayPoints = true, ShowSpawnPoints = true; - public const float LadderWaypointInterval = 100.0f; + public const float LadderWaypointInterval = 70.0f; protected SpawnType spawnType; private string[] idCardTags; @@ -99,6 +99,13 @@ namespace Barotrauma { SpawnType = SpawnType.Path; } + +#if CLIENT + if (SubEditorScreen.IsSubEditor()) + { + SubEditorScreen.StoreCommand(new AddOrDeleteCommand(new List { this }, false)); + } +#endif } @@ -179,42 +186,55 @@ namespace Barotrauma } } - float diffFromHullEdge = 50; - float minDist = 150.0f; + float minDist = 100.0f; float heightFromFloor = 110.0f; + float hullMinHeight = 100; foreach (Hull hull in Hull.hullList) { - if (hull.Rect.Height < 150) { continue; } - - WayPoint prevWaypoint = null; - + // Ignore hulls that a human couldn't fit in. + // Doesn't take multi-hull rooms into account, but it's probably best to leave them to be setup manually. + if (hull.Rect.Height < hullMinHeight) { continue; } + // Don't create waypoints if there's no floor. + Vector2 floorPos = new Vector2(hull.SimPosition.X, ConvertUnits.ToSimUnits(hull.Rect.Y - hull.RectHeight - 50)); + Body floor = Submarine.PickBody(hull.SimPosition, floorPos, collisionCategory: Physics.CollisionWall | Physics.CollisionPlatform, customPredicate: f => !(f.Body.UserData is Submarine)); + if (floor == null) { continue; } + // Make sure that the waypoints don't go higher than the halfway of the room. + float waypointHeight = hull.Rect.Height > heightFromFloor * 2 ? heightFromFloor : hull.Rect.Height / 2; if (hull.Rect.Width < diffFromHullEdge * 3.0f) { - new WayPoint( - new Vector2(hull.Rect.X + hull.Rect.Width / 2.0f, hull.Rect.Y - hull.Rect.Height + heightFromFloor), SpawnType.Path, submarine); - continue; + new WayPoint(new Vector2(hull.Rect.X + hull.Rect.Width / 2.0f, hull.Rect.Y - hull.Rect.Height + waypointHeight), SpawnType.Path, submarine); } - - for (float x = hull.Rect.X + diffFromHullEdge; x <= hull.Rect.Right - diffFromHullEdge; x += minDist) + else { - var wayPoint = new WayPoint(new Vector2(x, hull.Rect.Y - hull.Rect.Height + heightFromFloor), SpawnType.Path, submarine); - if (prevWaypoint != null) { wayPoint.ConnectTo(prevWaypoint); } - prevWaypoint = wayPoint; + WayPoint prevWaypoint = null; + for (float x = hull.Rect.X + diffFromHullEdge; x <= hull.Rect.Right - diffFromHullEdge; x += minDist) + { + var wayPoint = new WayPoint(new Vector2(x, hull.Rect.Y - hull.Rect.Height + waypointHeight), SpawnType.Path, submarine); + if (prevWaypoint != null) { wayPoint.ConnectTo(prevWaypoint); } + prevWaypoint = wayPoint; + } + if (prevWaypoint == null) + { + // Ensure that we always create at least one waypoint per hull. + new WayPoint(new Vector2(hull.Rect.X + hull.Rect.Width / 2.0f, hull.Rect.Y - hull.Rect.Height + waypointHeight), SpawnType.Path, submarine); + } } } - float outSideWaypointInterval = 200.0f; + float outSideWaypointInterval = 100.0f; if (submarine.Info.Type != SubmarineType.OutpostModule) { - int outsideWaypointDist = 100; + List outsideWaypoints = new List(); Rectangle borders = Hull.GetBorders(); - borders.X -= outsideWaypointDist; - borders.Y += outsideWaypointDist; - borders.Width += outsideWaypointDist * 2; - borders.Height += outsideWaypointDist * 2; + int originalWidth = borders.Width; + int originalHeight = borders.Height; + borders.X -= Math.Min(500, originalWidth / 4); + borders.Y += Math.Min(500, originalHeight / 4); + borders.Width += Math.Min(1500, originalWidth / 2); + borders.Height += Math.Min(1000, originalHeight / 2); borders.Location -= MathUtils.ToPoint(submarine.HiddenSubPosition); if (borders.Width <= outSideWaypointInterval * 2) @@ -238,6 +258,8 @@ namespace Barotrauma new Vector2(x, borders.Y - borders.Height * i) + submarine.HiddenSubPosition, SpawnType.Path, submarine); + outsideWaypoints.Add(wayPoint); + if (x == borders.X + outSideWaypointInterval) { cornerWaypoint[i, 0] = wayPoint; @@ -260,18 +282,107 @@ namespace Barotrauma new Vector2(borders.X + borders.Width * i, y) + submarine.HiddenSubPosition, SpawnType.Path, submarine); + outsideWaypoints.Add(wayPoint); + if (y == borders.Y - borders.Height) { wayPoint.ConnectTo(cornerWaypoint[1, i]); } else { - wayPoint.ConnectTo(WayPoint.WayPointList[WayPointList.Count - 2]); + wayPoint.ConnectTo(WayPointList[WayPointList.Count - 2]); } } wayPoint.ConnectTo(cornerWaypoint[0, i]); } + + Vector2 center = ConvertUnits.ToSimUnits(submarine.HiddenSubPosition); + float halfHeight = ConvertUnits.ToSimUnits(borders.Height / 2); + // Try to move the waypoints so that they are near the walls, roughly following the shape of the sub. + foreach (WayPoint wp in outsideWaypoints) + { + float xDiff = center.X - wp.SimPosition.X; + Vector2 targetPos = new Vector2(center.X - xDiff * 0.5f, center.Y); + Body wall = Submarine.PickBody(wp.SimPosition, targetPos, collisionCategory: Physics.CollisionWall, customPredicate: f => !(f.Body.UserData is Submarine)); + if (wall == null) + { + // Try again, and shoot to the center now. It happens with some subs that the first, offset raycast don't hit the walls. + targetPos = new Vector2(center.X - xDiff, center.Y); + wall = Submarine.PickBody(wp.SimPosition, targetPos, collisionCategory: Physics.CollisionWall, customPredicate: f => !(f.Body.UserData is Submarine)); + } + if (wall != null) + { + float distanceFromWall = 1; + if (xDiff > 0 && !submarine.Info.HasTag(SubmarineTag.Shuttle)) + { + // We don't want to move the waypoints near the tail too close to the engine. + float yDist = Math.Abs(center.Y - wp.SimPosition.Y); + distanceFromWall = MathHelper.Lerp(1, 3, MathUtils.InverseLerp(halfHeight, 0, yDist)); + } + Vector2 newPos = Submarine.LastPickedPosition + Submarine.LastPickedNormal * distanceFromWall; + wp.rect = new Rectangle(ConvertUnits.ToDisplayUnits(newPos).ToPoint(), wp.rect.Size); + wp.FindHull(); + } + } + // Remove unwanted points + var removals = new List(); + WayPoint previous = null; + float tooClose = outSideWaypointInterval / 2; + foreach (WayPoint wp in outsideWaypoints) + { + if (wp.CurrentHull != null || + Submarine.PickBody(wp.SimPosition, wp.SimPosition + Vector2.Normalize(center - wp.SimPosition) * 0.1f, collisionCategory: Physics.CollisionWall | Physics.CollisionItem, customPredicate: f => !(f.Body.UserData is Submarine), allowInsideFixture: true) != null) + { + // Remove waypoints that got inside/too near the sub. + removals.Add(wp); + previous = wp; + continue; + } + foreach (WayPoint otherWp in outsideWaypoints) + { + if (otherWp == wp) { continue; } + if (removals.Contains(otherWp)) { continue; } + float sqrDist = Vector2.DistanceSquared(wp.Position, otherWp.Position); + // Remove waypoints that are too close to each other. + if (!removals.Contains(previous) && sqrDist < tooClose * tooClose) + { + removals.Add(wp); + } + } + previous = wp; + } + foreach (WayPoint wp in removals) + { + outsideWaypoints.Remove(wp); + wp.Remove(); + } + // Connect loose ends (TODO: this sometimes fails, creating the connection to a wrong node) + for (int i = 0; i < outsideWaypoints.Count; i++) + { + WayPoint current = outsideWaypoints[i]; + if (current.linkedTo.Count > 1) { continue; } + WayPoint next = null; + int maxConnections = 2; + float tooFar = outSideWaypointInterval * 5; + for (int j = 0; j < maxConnections; j++) + { + if (current.linkedTo.Count >= maxConnections) { break; } + tooFar /= current.linkedTo.Count; + // First try to find a loose end + next = current.FindClosestOutside(outsideWaypoints, tolerance: tooFar, filter: wp => wp != next && wp.linkedTo.None(e => current.linkedTo.Contains(e)) && wp.linkedTo.Count < 2); + // Then accept any connection that not connected to the existing connection + next ??= current.FindClosestOutside(outsideWaypoints, tolerance: tooFar, filter: wp => wp != next && wp.linkedTo.None(e => current.linkedTo.Contains(e))); + if (next != null) + { + current.ConnectTo(next); + } + } + if (current.linkedTo.Count == 1) + { + DebugConsole.ThrowError($"Couldn't automatically link waypoint {current.ID}. You should do it manually."); + } + } } List stairList = new List(); @@ -298,13 +409,13 @@ namespace Barotrauma { for (int dir = -1; dir <= 1; dir += 2) { - WayPoint closest = stairPoints[i].FindClosest(dir, true, new Vector2(-30.0f, 30f)); - if (closest == null) continue; + WayPoint closest = stairPoints[i].FindClosest(dir, horizontalSearch: true, new Vector2(100, 70)); + if (closest == null) { continue; } stairPoints[i].ConnectTo(closest); } } - stairPoints[2] = new WayPoint((stairPoints[0].Position + stairPoints[1].Position)/2, SpawnType.Path, submarine); + stairPoints[2] = new WayPoint((stairPoints[0].Position + stairPoints[1].Position) / 2, SpawnType.Path, submarine); stairPoints[0].ConnectTo(stairPoints[2]); stairPoints[2].ConnectTo(stairPoints[1]); } @@ -314,21 +425,44 @@ namespace Barotrauma var ladders = item.GetComponent(); if (ladders == null) { continue; } + Vector2 bottomPoint = new Vector2(item.Rect.Center.X, item.Rect.Top - item.Rect.Height + 10); List ladderPoints = new List { - new WayPoint(new Vector2(item.Rect.Center.X, item.Rect.Y - item.Rect.Height + heightFromFloor), SpawnType.Path, submarine) + new WayPoint(bottomPoint, SpawnType.Path, submarine), }; - WayPoint prevPoint = ladderPoints[0]; - Vector2 prevPos = prevPoint.SimPosition; List ignoredBodies = new List(); - - for (float y = ladderPoints[0].Position.Y + LadderWaypointInterval; y < item.Rect.Y - 1.0f; y += LadderWaypointInterval) + // Lowest point is only meaningful for hanging ladders inside the sub, but it shouldn't matter in other cases either. + // Start point is where the bots normally grasp the ladder when they stand on ground. + WayPoint lowestPoint = ladderPoints[0]; + WayPoint prevPoint = lowestPoint; + Vector2 prevPos = prevPoint.SimPosition; + Body ground = Submarine.PickBody(lowestPoint.SimPosition, lowestPoint.SimPosition - Vector2.UnitY, ignoredBodies, + collisionCategory: Physics.CollisionWall | Physics.CollisionPlatform | Physics.CollisionStairs, + customPredicate: f => !(f.Body.UserData is Submarine)); + float startHeight = ground != null ? ConvertUnits.ToDisplayUnits(ground.Position.Y) : bottomPoint.Y; + startHeight += heightFromFloor; + WayPoint startPoint = lowestPoint; + Vector2 nextPos = new Vector2(item.Rect.Center.X, startHeight); + // Don't create the start point if it's too close to the lowest point or if it's outside of the sub. + // If we skip creating the start point, the lowest point is used instead. + if (lowestPoint == null || Math.Abs(startPoint.Position.Y - startHeight) > 40 && Hull.FindHull(nextPos) != null) + { + startPoint = new WayPoint(nextPos, SpawnType.Path, submarine); + ladderPoints.Add(startPoint); + if (lowestPoint != null) + { + startPoint.ConnectTo(lowestPoint); + } + prevPoint = startPoint; + prevPos = prevPoint.SimPosition; + } + for (float y = startPoint.Position.Y + LadderWaypointInterval; y < item.Rect.Y - 1.0f; y += LadderWaypointInterval) { //first check if there's a door in the way //(we need to create a waypoint linked to the door for NPCs to open it) Body pickedBody = Submarine.PickBody( - ConvertUnits.ToSimUnits(new Vector2(ladderPoints[0].Position.X, y)), + ConvertUnits.ToSimUnits(new Vector2(startPoint.Position.X, y)), prevPos, ignoredBodies, Physics.CollisionWall, false, (Fixture f) => f.Body.UserData is Item && ((Item)f.Body.UserData).GetComponent() != null); @@ -341,7 +475,7 @@ namespace Barotrauma { //no door, check for walls pickedBody = Submarine.PickBody( - ConvertUnits.ToSimUnits(new Vector2(ladderPoints[0].Position.X, y)), prevPos, ignoredBodies, null, false, + ConvertUnits.ToSimUnits(new Vector2(startPoint.Position.X, y)), prevPos, ignoredBodies, null, false, (Fixture f) => f.Body.UserData is Structure); } @@ -374,75 +508,94 @@ namespace Barotrauma } } - if (prevPoint.rect.Y < item.Rect.Y - 10.0f) + // Cap + if (prevPoint.rect.Y < item.Rect.Y - 40) { - WayPoint newPoint = new WayPoint(new Vector2(item.Rect.Center.X, item.Rect.Y - 1.0f), SpawnType.Path, submarine); - ladderPoints.Add(newPoint); - newPoint.ConnectTo(prevPoint); + WayPoint wayPoint = new WayPoint(new Vector2(item.Rect.Center.X, item.Rect.Y - 1.0f), SpawnType.Path, submarine); + ladderPoints.Add(wayPoint); + wayPoint.ConnectTo(prevPoint); } - - //connect ladder waypoints to hull points at the right and left side + + // Connect ladder waypoints to hull points at the right and left side foreach (WayPoint ladderPoint in ladderPoints) { ladderPoint.Ladders = ladders; - //don't connect if the waypoint is at a gap (= at the boundary of hulls and/or at a hatch) - if (ladderPoint.ConnectedGap != null) continue; - + bool isHatch = ladderPoint.ConnectedGap != null && !ladderPoint.ConnectedGap.IsRoomToRoom; for (int dir = -1; dir <= 1; dir += 2) { - WayPoint closest = ladderPoint.FindClosest(dir, true, new Vector2(-150.0f, 50f)); - if (closest == null) continue; + WayPoint closest = null; + if (isHatch) + { + closest = ladderPoint.FindClosest(dir, horizontalSearch: true, new Vector2(500, 1000), ladderPoint.ConnectedGap?.ConnectedDoor?.Body.FarseerBody, filter: wp => wp.CurrentHull == null, ignored: ladderPoints); + } + else + { + closest = ladderPoint.FindClosest(dir, horizontalSearch: true, new Vector2(150, 70), ladderPoint.ConnectedGap?.ConnectedDoor?.Body.FarseerBody, ignored: ladderPoints); + } + if (closest == null) { continue; } ladderPoint.ConnectTo(closest); } } } - - foreach (Gap gap in Gap.GapList) + + // Another pass: connect cap and bottom points with other ladders when they are vertically adjacent to another (double ladders) + foreach (Item item in Item.ItemList) { - if (!gap.IsHorizontal) continue; - - //too small to walk through - if (gap.Rect.Height < 150.0f) continue; - - var wayPoint = new WayPoint( - new Vector2(gap.Rect.Center.X, gap.Rect.Y - gap.Rect.Height + heightFromFloor), SpawnType.Path, submarine, gap); - - for (int dir = -1; dir <= 1; dir += 2) - { - float tolerance = gap.IsRoomToRoom ? 50.0f : outSideWaypointInterval / 2.0f; - - WayPoint closest = wayPoint.FindClosest( - dir, true, new Vector2(-tolerance, tolerance), - gap.ConnectedDoor?.Body.FarseerBody); - - if (closest != null) - { - wayPoint.ConnectTo(closest); - } - } + var ladders = item.GetComponent(); + if (ladders == null) { continue; } + var wps = WayPointList.Where(wp => wp.Ladders == ladders).OrderByDescending(wp => wp.Rect.Y); + WayPoint cap = wps.First(); + WayPoint above = cap.FindClosest(1, horizontalSearch: false, tolerance: new Vector2(25, 50), filter: wp => wp.Ladders != null && wp.Ladders != ladders); + above?.ConnectTo(cap); + WayPoint bottom = wps.Last(); + WayPoint below = bottom.FindClosest(-1, horizontalSearch: false, tolerance: new Vector2(25, 50), filter: wp => wp.Ladders != null && wp.Ladders != ladders); + below?.ConnectTo(bottom); } foreach (Gap gap in Gap.GapList) { - if (gap.IsHorizontal || gap.IsRoomToRoom || !gap.linkedTo.Any(l => l is Hull)) { continue; } - - //too small to walk through - if (gap.Rect.Width < 100.0f) { continue; } - - var wayPoint = new WayPoint( - new Vector2(gap.Rect.Center.X, gap.Rect.Y - gap.Rect.Height / 2), SpawnType.Path, submarine, gap); - - float tolerance = outSideWaypointInterval / 2.0f; - Hull connectedHull = (Hull)gap.linkedTo.First(l => l is Hull); - int dir = Math.Sign(connectedHull.Position.Y - gap.Position.Y); - - WayPoint closest = wayPoint.FindClosest( - dir, false, new Vector2(-tolerance, tolerance), - gap.ConnectedDoor?.Body.FarseerBody); - - if (closest != null) + if (gap.IsHorizontal) { - wayPoint.ConnectTo(closest); + // Too small to walk through + if (gap.Rect.Height < hullMinHeight) { continue; } + Vector2 pos = new Vector2(gap.Rect.Center.X, gap.Rect.Y - gap.Rect.Height + heightFromFloor); + var wayPoint = new WayPoint(pos, SpawnType.Path, submarine, gap); + // The closest waypoint can be quite far if the gap is at an exterior door. + Vector2 tolerance = gap.IsRoomToRoom ? new Vector2(150, 70) : new Vector2(1000, 1000); + for (int dir = -1; dir <= 1; dir += 2) + { + WayPoint closest = wayPoint.FindClosest(dir, horizontalSearch: true, tolerance, gap.ConnectedDoor?.Body.FarseerBody); + if (closest != null) + { + wayPoint.ConnectTo(closest); + } + } + } + else + { + // Create waypoints on vertical gaps on the outer walls, also hatches. + if (gap.IsRoomToRoom || gap.linkedTo.None(l => l is Hull)) { continue; } + // Too small to swim through + if (gap.Rect.Width < 50.0f) { continue; } + Vector2 pos = new Vector2(gap.Rect.Center.X, gap.Rect.Y - gap.Rect.Height / 2); + // Some hatches are created in the block above where we handle the ladder waypoints. So we need to check for duplicates. + if (WayPointList.Any(wp => wp.ConnectedGap == gap)) { continue; } + var wayPoint = new WayPoint(pos, SpawnType.Path, submarine, gap); + Hull connectedHull = (Hull)gap.linkedTo.First(l => l is Hull); + int dir = Math.Sign(connectedHull.Position.Y - gap.Position.Y); + WayPoint closest = wayPoint.FindClosest(dir, horizontalSearch: false, new Vector2(50, 100)); + if (closest != null) + { + wayPoint.ConnectTo(closest); + } + for (dir = -1; dir <= 1; dir += 2) + { + closest = wayPoint.FindClosest(dir, horizontalSearch: true, new Vector2(500, 1000), gap.ConnectedDoor?.Body.FarseerBody, filter: wp => wp.CurrentHull == null); + if (closest != null) + { + wayPoint.ConnectTo(closest); + } + } } } @@ -462,7 +615,36 @@ namespace Barotrauma return true; } - private WayPoint FindClosest(int dir, bool horizontalSearch, Vector2 tolerance, Body ignoredBody = null) + private WayPoint FindClosestOutside(IEnumerable waypointList, float tolerance, Body ignoredBody = null, IEnumerable ignored = null, Func filter = null) + { + float closestDist = 0; + WayPoint closest = null; + foreach (WayPoint wp in waypointList) + { + if (wp.SpawnType != SpawnType.Path || wp == this) { continue; } + // Ignore if already linked + if (linkedTo.Contains(wp)) { continue; } + if (ignored != null && ignored.Contains(wp)) { continue; } + if (filter != null && !filter(wp)) { continue; } + float sqrDist = Vector2.DistanceSquared(Position, wp.Position); + if (closest == null || sqrDist < closestDist) + { + var body = Submarine.CheckVisibility(SimPosition, wp.SimPosition, ignoreLevel: true, ignoreSubs: true, ignoreSensors: false); + if (body != null && body != ignoredBody && !(body.UserData is Submarine)) + { + if (body.UserData is Structure || body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) + { + continue; + } + } + closestDist = sqrDist; + closest = wp; + } + } + return closest; + } + + private WayPoint FindClosest(int dir, bool horizontalSearch, Vector2 tolerance, Body ignoredBody = null, IEnumerable ignored = null, Func filter = null) { if (dir != -1 && dir != 1) { return null; } @@ -473,33 +655,45 @@ namespace Barotrauma { if (wp.SpawnType != SpawnType.Path || wp == this) { continue; } + float xDiff = wp.Position.X - Position.X; + float yDiff = wp.Position.Y - Position.Y; + float xDist = Math.Abs(xDiff); + float yDist = Math.Abs(yDiff); + if (tolerance.X < xDist) { continue; } + if (tolerance.Y < yDist) { continue; } + float dist = 0.0f; float diff = 0.0f; if (horizontalSearch) { - if ((wp.Position.Y - Position.Y) < tolerance.X || (wp.Position.Y - Position.Y) > tolerance.Y) { continue; } - diff = wp.Position.X - Position.X; - dist = Math.Abs(diff) + Math.Abs(wp.Position.Y - Position.Y) / 5.0f; + diff = xDiff; + dist = xDist + yDist / 5.0f; } else { - if ((wp.Position.X - Position.X) < tolerance.X || (wp.Position.X - Position.X) > tolerance.Y) { continue; } - diff = wp.Position.Y - Position.Y; - dist = Math.Abs(diff) + Math.Abs(wp.Position.X - Position.X) / 5.0f; + diff = yDiff; + dist = yDist + xDist / 5.0f; //prefer ladder waypoints when moving vertically if (wp.Ladders != null) { dist *= 0.5f; } } if (Math.Sign(diff) != dir) { continue; } + // Ignore if already linked + if (linkedTo.Contains(wp)) { continue; } + if (ignored != null && ignored.Contains(wp)) { continue; } + if (filter != null && !filter(wp)) { continue; } if (closest == null || dist < closestDist) { - var body = Submarine.CheckVisibility(SimPosition, wp.SimPosition, true, true, false); + var body = Submarine.CheckVisibility(SimPosition, wp.SimPosition, ignoreLevel: true, ignoreSubs: true, ignoreSensors: false); if (body != null && body != ignoredBody && !(body.UserData is Submarine)) { - if (body.UserData is Structure || body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { continue; } + if (body.UserData is Structure || body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) + { + continue; + } } - + closestDist = dist; closest = wp; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs index 39cba8d86..d4c48430a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs @@ -209,11 +209,11 @@ namespace Barotrauma.Networking case ChatMessageType.Order: if (receiver != null && !receiver.IsDead) { - var receiverItem = receiver.Inventory?.Items.FirstOrDefault(i => i?.GetComponent() != null); + var receiverItem = receiver.Inventory?.AllItems.FirstOrDefault(i => i.GetComponent() != null); //character doesn't have a radio -> don't send if (receiverItem == null || !receiver.HasEquippedItem(receiverItem)) { return spokenMsg; } - var senderItem = sender.Inventory?.Items.FirstOrDefault(i => i?.GetComponent() != null); + var senderItem = sender.Inventory?.AllItems.FirstOrDefault(i => i.GetComponent() != null); if (senderItem == null || !sender.HasEquippedItem(senderItem)) { return spokenMsg; } var receiverRadio = receiverItem.GetComponent(); @@ -253,7 +253,7 @@ namespace Barotrauma.Networking { radio = null; if (sender?.Inventory == null || sender.Removed) { return false; } - radio = sender.Inventory.Items.FirstOrDefault(i => i?.GetComponent() != null)?.GetComponent(); + radio = sender.Inventory.AllItems.FirstOrDefault(i => i.GetComponent() != null)?.GetComponent(); if (radio?.Item == null) { return false; } return sender.HasEquippedItem(radio.Item) && radio.CanTransmit(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs index 37fd779ab..9a4c83272 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs @@ -20,7 +20,9 @@ namespace Barotrauma.Networking public string PreferredJob; - public Character.TeamType TeamID; + public CharacterTeamType TeamID; + + public CharacterTeamType PreferredTeam; private Character character; public Character Character diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs index eaf19d174..9242f92cc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs @@ -27,6 +27,7 @@ namespace Barotrauma public readonly float Condition; public bool SpawnIfInventoryFull = true; + public bool IgnoreLimbSlots = false; private readonly Action onSpawned; @@ -64,13 +65,27 @@ namespace Barotrauma Item spawnedItem; if (Inventory?.Owner != null) { - if (!SpawnIfInventoryFull && !Inventory.Items.Any(it => it == null)) + if (!SpawnIfInventoryFull && !Inventory.CanBePut(Prefab)) { return null; } - spawnedItem = new Item(Prefab, Vector2.Zero, null); + spawnedItem = new Item(Prefab, Vector2.Zero, null) + { + Condition = Condition + }; if (!Inventory.Owner.Removed && !Inventory.TryPutItem(spawnedItem, null, spawnedItem.AllowedSlots)) { + if (IgnoreLimbSlots) + { + for (int i = 0; i < Inventory.Capacity; i++) + { + if (Inventory.GetItemAt(i) == null) + { + Inventory.ForceToSlot(spawnedItem, i); + break; + } + } + } spawnedItem.SetTransform(FarseerPhysics.ConvertUnits.ToSimUnits(Inventory.Owner?.WorldPosition ?? Vector2.Zero), spawnedItem.body?.Rotation ?? 0.0f, findNewHull: false); } } @@ -244,7 +259,7 @@ namespace Barotrauma spawnQueue.Enqueue(new ItemSpawnInfo(itemPrefab, position, sub, onSpawned, condition)); } - public void AddToSpawnQueue(ItemPrefab itemPrefab, Inventory inventory, float? condition = null, Action onSpawned = null, bool spawnIfInventoryFull = true) + public void AddToSpawnQueue(ItemPrefab itemPrefab, Inventory inventory, float? condition = null, Action onSpawned = null, bool spawnIfInventoryFull = true, bool ignoreLimbSlots = false) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } if (itemPrefab == null) @@ -254,7 +269,11 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue3:ItemPrefabNull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); return; } - spawnQueue.Enqueue(new ItemSpawnInfo(itemPrefab, inventory, onSpawned, condition) { SpawnIfInventoryFull = spawnIfInventoryFull }); + spawnQueue.Enqueue(new ItemSpawnInfo(itemPrefab, inventory, onSpawned, condition) + { + SpawnIfInventoryFull = spawnIfInventoryFull, + IgnoreLimbSlots = ignoreLimbSlots + }); } public void AddToSpawnQueue(string speciesName, Vector2 worldPosition, Action onSpawn = null) @@ -308,7 +327,7 @@ namespace Barotrauma if (removeQueue.Contains(item) || item.Removed) { return; } removeQueue.Enqueue(item); - var containedItems = item.OwnInventory?.Items; + var containedItems = item.OwnInventory?.AllItems; if (containedItems == null) { return; } foreach (Item containedItem in containedItems) { @@ -335,6 +354,11 @@ namespace Barotrauma return spawnQueue.Count(s => predicate(s)); } + public bool IsInRemoveQueue(Entity entity) + { + return removeQueue.Contains(entity); + } + public void Update() { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/KarmaManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/KarmaManager.cs index f4b0eb0b1..38a6c5f5e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/KarmaManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/KarmaManager.cs @@ -36,7 +36,7 @@ namespace Barotrauma [Serialize(0.1f, true)] public float StructureDamageKarmaDecrease { get; set; } - [Serialize(30.0f, true)] + [Serialize(15.0f, true)] public float MaxStructureDamageKarmaDecreasePerSecond { get; set; } [Serialize(0.03f, true)] diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEventManager.cs index 2f9138c36..4a5441b57 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEventManager.cs @@ -44,7 +44,8 @@ namespace Barotrauma.Networking continue; } - if (msg.LengthBytes + tempBuffer.LengthBytes + tempEventBuffer.LengthBytes > MaxEventBufferLength) + if (eventCount > 0 && + msg.LengthBytes + tempBuffer.LengthBytes + tempEventBuffer.LengthBytes > MaxEventBufferLength) { //no more room in this packet break; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs index 864233654..709c4d002 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs @@ -226,13 +226,13 @@ namespace Barotrauma.Networking public bool CanUseRadio(Character sender) { - if (sender == null) return false; + if (sender == null) { return false; } - var radio = sender.Inventory.Items.FirstOrDefault(i => i != null && i.GetComponent() != null); - if (radio == null || !sender.HasEquippedItem(radio)) return false; + var radio = sender.Inventory.AllItems.FirstOrDefault(i => i.GetComponent() != null); + if (radio == null || !sender.HasEquippedItem(radio)) { return false; } var radioComponent = radio.GetComponent(); - if (radioComponent == null) return false; + if (radioComponent == null) { return false; } return radioComponent.HasRequiredContainedItems(sender, addMessage: false); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs index 0ac054cdc..183c48e38 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs @@ -79,7 +79,7 @@ namespace Barotrauma.Networking } foreach (WifiComponent wifiComponent in wifiComponents) { - wifiComponent.TeamID = Character.TeamType.FriendlyNPC; + wifiComponent.TeamID = CharacterTeamType.FriendlyNPC; } ResetShuttle(); @@ -222,7 +222,7 @@ namespace Barotrauma.Networking foreach (Item item in Item.ItemList) { - if (item.Submarine != RespawnShuttle) continue; + if (item.Submarine != RespawnShuttle) { continue; } //remove respawn items that have been left in the shuttle if (respawnItems.Contains(item)) @@ -239,6 +239,19 @@ namespace Barotrauma.Networking { powerContainer.Charge = powerContainer.Capacity; } + + var door = item.GetComponent(); + if (door != null) { door.Stuck = 0.0f; } + + var steering = item.GetComponent(); + if (steering != null) + { + steering.MaintainPos = true; + steering.AutoPilot = true; +#if SERVER + steering.UnsentChanges = true; +#endif + } } foreach (Structure wall in Structure.WallList) @@ -269,9 +282,8 @@ namespace Barotrauma.Networking Spawner.AddToRemoveQueue(c); if (c.Inventory != null) { - foreach (Item item in c.Inventory.Items) + foreach (Item item in c.Inventory.AllItems) { - if (item == null) continue; Spawner.AddToRemoveQueue(item); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs index 540958266..aeb5d17ed 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs @@ -749,15 +749,21 @@ namespace Barotrauma public void MoveToPos(Vector2 simPosition, float force, Vector2? pullPos = null) { - if (pullPos == null) pullPos = FarseerBody.Position; + if (pullPos == null) { pullPos = FarseerBody.Position; } - if (!IsValidValue(simPosition, "position", -1e10f, 1e10f)) return; - if (!IsValidValue(force, "force")) return; + if (!IsValidValue(simPosition, "position", -1e10f, 1e10f)) { return; } + if (!IsValidValue(force, "force")) { return; } Vector2 vel = FarseerBody.LinearVelocity; Vector2 deltaPos = simPosition - (Vector2)pullPos; + if (deltaPos.LengthSquared() > 100.0f * 100.0f) + { +#if DEBUG || UNSTABLE + DebugConsole.ThrowError("Attempted to move a physics body to an invalid position.\n" + Environment.StackTrace.CleanupStackTrace()); +#endif + } deltaPos *= force; - FarseerBody.ApplyLinearImpulse((deltaPos - vel * 0.5f) * FarseerBody.Mass, (Vector2)pullPos); + ApplyLinearImpulse((deltaPos - vel * 0.5f) * FarseerBody.Mass, (Vector2)pullPos); } /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs index fb736fba5..def402528 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs @@ -97,25 +97,23 @@ namespace Barotrauma #if DEBUG && CLIENT - if (GameMain.GameSession != null && GameMain.GameSession.Level != null && GameMain.GameSession.Submarine != null && - !DebugConsole.IsOpen && GUI.KeyboardDispatcher.Subscriber == null) + if (GameMain.GameSession != null && !DebugConsole.IsOpen && GUI.KeyboardDispatcher.Subscriber == null) { - if (PlayerInput.KeyHit(Keys.Insert)) + if (GameMain.GameSession.Level != null && GameMain.GameSession.Submarine != null) { - DebugConsole.ExecuteCommand("teleportcharacter"); + Submarine closestSub = Submarine.FindClosest(cam.WorldViewCenter) ?? GameMain.GameSession.Submarine; + + Vector2 targetMovement = Vector2.Zero; + if (PlayerInput.KeyDown(Keys.I)) { targetMovement.Y += 1.0f; } + if (PlayerInput.KeyDown(Keys.K)) { targetMovement.Y -= 1.0f; } + if (PlayerInput.KeyDown(Keys.J)) { targetMovement.X -= 1.0f; } + if (PlayerInput.KeyDown(Keys.L)) { targetMovement.X += 1.0f; } + + if (targetMovement != Vector2.Zero) + { + closestSub.ApplyForce(targetMovement * closestSub.SubBody.Body.Mass * 100.0f); + } } - - var closestSub = Submarine.FindClosest(cam.WorldViewCenter); - if (closestSub == null) closestSub = GameMain.GameSession.Submarine; - - Vector2 targetMovement = Vector2.Zero; - if (PlayerInput.KeyDown(Keys.I)) targetMovement.Y += 1.0f; - if (PlayerInput.KeyDown(Keys.K)) targetMovement.Y -= 1.0f; - if (PlayerInput.KeyDown(Keys.J)) targetMovement.X -= 1.0f; - if (PlayerInput.KeyDown(Keys.L)) targetMovement.X += 1.0f; - - if (targetMovement != Vector2.Zero) - closestSub.ApplyForce(targetMovement * closestSub.SubBody.Body.Mass * 100.0f); } #endif @@ -158,9 +156,8 @@ namespace Barotrauma } if (Character.Controlled.Inventory != null) { - foreach (Item item in Character.Controlled.Inventory.Items) + foreach (Item item in Character.Controlled.Inventory.AllItems) { - if (item == null) { continue; } if (Character.Controlled.HasEquippedItem(item)) { item.UpdateHUD(cam, Character.Controlled, (float)deltaTime); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index cefc33983..0a21bcdce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs @@ -660,9 +660,14 @@ namespace Barotrauma } public static bool IsOverride(this XElement element) => element.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase); + public static bool IsCharacterVariant(this XElement element) => element.Name.ToString().Equals("charactervariant", StringComparison.OrdinalIgnoreCase); public static XElement FirstElement(this XElement element) => element.Elements().FirstOrDefault(); + public static XAttribute GetAttribute(this XElement element, string name, StringComparison comparisonMethod = StringComparison.OrdinalIgnoreCase) => element.GetAttribute(a => a.Name.ToString().Equals(name, comparisonMethod)); + + public static XAttribute GetAttribute(this XElement element, Func predicate) => element.Attributes().FirstOrDefault(predicate); + /// /// Returns the first child element that matches the name using the provided comparison method. /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs b/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs index fa7e514ca..5ff8bbccd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs @@ -324,7 +324,7 @@ namespace Barotrauma { if (!path.EndsWith("/")) path += "/"; } - FilePath = path + file; + FilePath = (path + file).CleanUpPathCrossPlatform(correctFilenameCase: true); if (!string.IsNullOrEmpty(FilePath)) { FullPath = Path.GetFullPath(FilePath); diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs index ac1f25a7a..0d74a8e66 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs @@ -56,7 +56,7 @@ namespace Barotrauma { if (this.type != type || !HasRequiredItems(entity)) { return; } if (!Stackable && DelayList.Any(d => d.Parent == this && d.Targets.FirstOrDefault() == target)) { return; } - if (targetIdentifiers != null && !IsValidTarget(target)) { return; } + if (!IsValidTarget(target)) { return; } if (!HasRequiredConditions(target.ToEnumerable())) { return; } switch (delayType) @@ -92,11 +92,7 @@ namespace Barotrauma currentTargets.Clear(); foreach (ISerializableEntity target in targets) { - if (targetIdentifiers != null) - { - //ignore invalid targets - if (!IsValidTarget(target)) { continue; } - } + if (!IsValidTarget(target)) { continue; } currentTargets.Add(target); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs index a3e280b8f..6925b0dac 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs @@ -10,7 +10,7 @@ namespace Barotrauma // - Use XElement instead of XAttribute in the constructor // - Simplify, remove unnecessary conversions // - Improve the flow so that the logic is undestandable. - // - Maybe ass some test cases for the operators? + // - Maybe add some test cases for the operators? class PropertyConditional { public enum ConditionType @@ -18,6 +18,7 @@ namespace Barotrauma PropertyValue, Name, SpeciesName, + SpeciesGroup, HasTag, HasStatusTag, Affliction, @@ -46,6 +47,7 @@ namespace Barotrauma public readonly OperatorType Operator; public readonly string AttributeName; public readonly string AttributeValue; + public readonly string[] SplitAttributeValue; public readonly float? FloatValue; public readonly string TargetItemComponentName; @@ -55,8 +57,8 @@ namespace Barotrauma // Only used by conditionals targeting an item (makes the conditional check the item/character whose inventory this item is inside) public readonly bool TargetContainer; - - private readonly int cancelStatusEffect; + // Only used by conditionals targeting an item. By default, containers check the parent item. This allows you to check the grandparent instead. + public readonly bool TargetGrandParent; // Remove this after refactoring public static bool IsValid(XAttribute attribute) @@ -109,20 +111,7 @@ namespace Barotrauma TargetItemComponentName = attribute.Parent.GetAttributeString("targetitemcomponent", ""); TargetContainer = attribute.Parent.GetAttributeBool("targetcontainer", false); TargetSelf = attribute.Parent.GetAttributeBool("targetself", false); - - foreach (XElement subElement in attribute.Parent.Elements()) - { - switch (subElement.Name.ToString().ToLowerInvariant()) - { - case "cancel": - case "canceleffect": - case "cancelstatuseffect": - //This only works if there's a conditional checking for status effect tags. There is no way to cancel *all* status effects atm. - cancelStatusEffect = 1; - if (subElement.GetAttributeBool("all", false)) cancelStatusEffect = 2; - break; - } - } + TargetGrandParent = attribute.Parent.GetAttributeBool("targetgrandparent", false); if (!Enum.TryParse(AttributeName, true, out Type)) { @@ -137,6 +126,7 @@ namespace Barotrauma } AttributeValue = valueString; + SplitAttributeValue = valueString.Split(','); if (float.TryParse(AttributeValue, NumberStyles.Float, CultureInfo.InvariantCulture, out float value)) { FloatValue = value; @@ -180,8 +170,7 @@ namespace Barotrauma } public bool Matches(ISerializableEntity target) - { - string valStr = AttributeValue.ToString(); + { switch (Type) { case ConditionType.PropertyValue: @@ -194,13 +183,12 @@ namespace Barotrauma return false; case ConditionType.Name: if (target == null) { return Operator == OperatorType.NotEquals; } - return (Operator == OperatorType.Equals) == (target.Name == valStr); + return (Operator == OperatorType.Equals) == (target.Name == AttributeValue); case ConditionType.HasTag: { if (target == null) { return Operator == OperatorType.NotEquals; } - string[] readTags = valStr.Split(','); int matches = 0; - foreach (string tag in readTags) + foreach (string tag in SplitAttributeValue) { if (target is Item item && item.HasTag(tag)) { @@ -208,58 +196,38 @@ namespace Barotrauma } } //If operator is == then it needs to match everything, otherwise if its != there must be zero matches. - return Operator == OperatorType.Equals ? matches >= readTags.Length : matches <= 0; + return Operator == OperatorType.Equals ? matches >= SplitAttributeValue.Length : matches <= 0; } case ConditionType.HasStatusTag: if (target == null) { return Operator == OperatorType.NotEquals; } bool success = false; if (StatusEffect.DurationList.Any(d => d.Targets.Contains(target)) || DelayedEffect.DelayList.Any(d => d.Targets.Contains(target))) { - string[] readTags = valStr.Split(','); - foreach (DurationListElement duration in StatusEffect.DurationList) + int matches = 0; + foreach (DurationListElement durationEffect in StatusEffect.DurationList) { - if (!duration.Targets.Contains(target)) { continue; } - int matches = 0; - foreach (string tag in readTags) + if (!durationEffect.Targets.Contains(target)) { continue; } + foreach (string tag in SplitAttributeValue) { - if (duration.Parent.HasTag(tag)) + if (durationEffect.Parent.HasTag(tag)) { matches++; } } - success = Operator == OperatorType.Equals ? matches >= readTags.Length : matches <= 0; - if (cancelStatusEffect > 0 && success) - { - StatusEffect.DurationList.Remove(duration); - } - if (cancelStatusEffect != 2) - { - //cancelStatusEffect 1 = only cancel once, cancelStatusEffect 2 = cancel all of matching tags - return success; - } + success = Operator == OperatorType.Equals ? matches >= SplitAttributeValue.Length : matches <= 0; } - foreach (DelayedListElement delay in DelayedEffect.DelayList) + foreach (DelayedListElement delayedEffect in DelayedEffect.DelayList) { - if (!delay.Targets.Contains(target)) { continue; } - int matches = 0; - foreach (string tag in readTags) + if (!delayedEffect.Targets.Contains(target)) { continue; } + foreach (string tag in SplitAttributeValue) { - if (delay.Parent.HasTag(tag)) + if (delayedEffect.Parent.HasTag(tag)) { matches++; } } - success = Operator == OperatorType.Equals ? matches >= readTags.Length : matches <= 0; - if (cancelStatusEffect > 0 && success) - { - DelayedEffect.DelayList.Remove(delay); - } - if (cancelStatusEffect != 2) - { - //ditto - return success; - } } + return Operator == OperatorType.Equals ? matches >= SplitAttributeValue.Length : matches <= 0; } else if (Operator == OperatorType.NotEquals) { @@ -268,11 +236,19 @@ namespace Barotrauma } return success; case ConditionType.SpeciesName: - if (target == null) { return Operator == OperatorType.NotEquals; } - if (!(target is Character targetCharacter)) { return false; } - return (Operator == OperatorType.Equals) == targetCharacter.SpeciesName.Equals(valStr, StringComparison.OrdinalIgnoreCase); + { + if (target == null) { return Operator == OperatorType.NotEquals; } + if (!(target is Character targetCharacter)) { return false; } + return Operator == OperatorType.Equals == targetCharacter.SpeciesName.Equals(AttributeValue, StringComparison.OrdinalIgnoreCase); + } + case ConditionType.SpeciesGroup: + { + if (target == null) { return Operator == OperatorType.NotEquals; } + if (!(target is Character targetCharacter)) { return false; } + return Operator == OperatorType.Equals == targetCharacter.Params.CompareGroup(AttributeValue); + } case ConditionType.EntityType: - switch (valStr) + switch (AttributeValue) { case "character": case "Character": @@ -299,7 +275,7 @@ namespace Barotrauma } else { - return limb.type.ToString().Equals(valStr, StringComparison.OrdinalIgnoreCase); + return limb.type.ToString().Equals(AttributeValue, StringComparison.OrdinalIgnoreCase); } } case ConditionType.Affliction: diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index acf79c149..4ba9b0e7a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -1,5 +1,6 @@ using Barotrauma.Extensions; using Barotrauma.Items.Components; +using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -202,8 +203,9 @@ namespace Barotrauma private readonly List spawnItems; private readonly List spawnCharacters; - private List triggeredEvents; - private string triggeredEventTag = "statuseffect"; + private readonly List triggeredEvents; + private readonly string triggeredEventTargetTag = "statuseffecttarget", + triggeredEventEntityTag = "statuseffectentity"; private Character user; @@ -319,7 +321,7 @@ namespace Barotrauma foreach (XAttribute attribute in attributes) { - switch (attribute.Name.ToString()) + switch (attribute.Name.ToString().ToLowerInvariant()) { case "type": if (!Enum.TryParse(attribute.Value, true, out type)) @@ -361,8 +363,11 @@ namespace Barotrauma lifeTime = attribute.GetAttributeFloat(0); lifeTimer = lifeTime; break; - case "eventtag": - triggeredEventTag = attribute.Value; + case "eventtargettag": + triggeredEventTargetTag = attribute.Value; + break; + case "evententitytag": + triggeredEventEntityTag = attribute.Value; break; case "checkconditionalalways": CheckConditionalAlways = attribute.GetAttributeBool(false); @@ -524,6 +529,12 @@ namespace Barotrauma triggeredEvents.Add(prefab); } } + + foreach (XElement eventElement in subElement.Elements()) + { + if (!eventElement.Name.ToString().Equals("ScriptedEvent", StringComparison.OrdinalIgnoreCase)) { continue; } + triggeredEvents.Add(new EventPrefab(eventElement)); + } break; case "spawncharacter": var newSpawnCharacter = new CharacterSpawnInfo(subElement, parentDebugName); @@ -627,25 +638,30 @@ namespace Barotrauma public bool HasRequiredConditions(IEnumerable targets) { - return HasRequiredConditions(targets, targetingContainer: false); + return HasRequiredConditions(targets, propertyConditionals); } - private bool HasRequiredConditions(IEnumerable targets, bool targetingContainer) + private bool HasRequiredConditions(IEnumerable targets, IEnumerable conditionals, bool targetingContainer = false) { - if (!propertyConditionals.Any()) { return true; } - if (requiredItems.Any() && requiredItems.All(ri => ri.MatchOnEmpty) && !targets.Any()) { return true; } + if (conditionals.None()) { return true; } + if (requiredItems.Any() && requiredItems.All(ri => ri.MatchOnEmpty) && targets.None()) { return true; } switch (conditionalComparison) { case PropertyConditional.Comparison.Or: - foreach (PropertyConditional pc in propertyConditionals) + foreach (PropertyConditional pc in conditionals) { if (pc.TargetContainer && !targetingContainer) { var target = targets.FirstOrDefault(t => t is Item || t is ItemComponent); var targetItem = target as Item ?? (target as ItemComponent)?.Item; if (targetItem?.ParentInventory == null) { continue; } - if (targetItem.ParentInventory.Owner is Item container && HasRequiredConditions(container.AllPropertyObjects, targetingContainer: true)) { return true; } - if (targetItem.ParentInventory.Owner is Character character && HasRequiredConditions(character.ToEnumerable(), targetingContainer: true)) { return true; } + var owner = targetItem.ParentInventory.Owner; + if (pc.TargetGrandParent && owner is Item ownerItem) + { + owner = ownerItem.ParentInventory?.Owner; + } + if (owner is Item container && HasRequiredConditions(container.AllPropertyObjects, pc.ToEnumerable(), targetingContainer: true)) { return true; } + if (owner is Character character && HasRequiredConditions(character.ToEnumerable(), pc.ToEnumerable(), targetingContainer: true)) { return true; } } else { @@ -664,15 +680,20 @@ namespace Barotrauma } return false; case PropertyConditional.Comparison.And: - foreach (PropertyConditional pc in propertyConditionals) + foreach (PropertyConditional pc in conditionals) { if (pc.TargetContainer && !targetingContainer) { var target = targets.FirstOrDefault(t => t is Item || t is ItemComponent); var targetItem = target as Item ?? (target as ItemComponent)?.Item; if (targetItem?.ParentInventory == null) { return false; } - if (targetItem.ParentInventory.Owner is Item container && !HasRequiredConditions(container.AllPropertyObjects, targetingContainer: true)) { return false; } - if (targetItem.ParentInventory.Owner is Character character && !HasRequiredConditions(character.ToEnumerable(), targetingContainer: true)) { return false; } + var owner = targetItem.ParentInventory.Owner; + if (pc.TargetGrandParent && owner is Item ownerItem) + { + owner = ownerItem.ParentInventory?.Owner; + } + if (owner is Item container && !HasRequiredConditions(container.AllPropertyObjects, pc.ToEnumerable(), targetingContainer: true)) { return false; } + if (owner is Character character && !HasRequiredConditions(character.ToEnumerable(), pc.ToEnumerable(), targetingContainer: true)) { return false; } } else { @@ -685,8 +706,8 @@ namespace Barotrauma continue; } } - if (!pc.Matches(target)) { return false; } } + if (targets.None(t => pc.Matches(t))) { return false; } } } return true; @@ -697,8 +718,6 @@ namespace Barotrauma protected bool IsValidTarget(ISerializableEntity entity) { - if (targetIdentifiers == null) { return true; } - if (entity is Item item) { return IsValidTarget(item); @@ -709,6 +728,7 @@ namespace Barotrauma } else if (entity is Structure structure) { + if (targetIdentifiers == null) { return true; } if (targetIdentifiers.Contains("structure")) { return true; } if (targetIdentifiers.Any(id => id.Equals(structure.Prefab.Identifier, StringComparison.OrdinalIgnoreCase))) { return true; } } @@ -716,7 +736,7 @@ namespace Barotrauma { return IsValidTarget(character); } - + if (targetIdentifiers == null) { return true; } return targetIdentifiers.Any(id => id.Equals(entity.Name, StringComparison.OrdinalIgnoreCase)); } @@ -724,6 +744,7 @@ namespace Barotrauma { if (OnlyInside && itemComponent.Item.CurrentHull == null) { return false; } if (OnlyOutside && itemComponent.Item.CurrentHull != null) { return false; } + if (targetIdentifiers == null) { return true; } if (targetIdentifiers.Contains("itemcomponent")) { return true; } if (itemComponent.Item.HasTag(targetIdentifiers)) { return true; } return targetIdentifiers.Any(id => id.Equals(itemComponent.Item.Prefab.Identifier, StringComparison.OrdinalIgnoreCase)); @@ -761,7 +782,7 @@ namespace Barotrauma { if (this.type != type || !HasRequiredItems(entity)) { return; } - if (targetIdentifiers != null && !IsValidTarget(target)) { return; } + if (!IsValidTarget(target)) { return; } if (duration > 0.0f && !Stackable) { @@ -786,11 +807,7 @@ namespace Barotrauma currentTargets.Clear(); foreach (ISerializableEntity target in targets) { - if (targetIdentifiers != null) - { - //ignore invalid targets - if (!IsValidTarget(target)) { continue; } - } + if (!IsValidTarget(target)) { continue; } currentTargets.Add(target); } @@ -943,9 +960,9 @@ namespace Barotrauma { if (targetEntity.Removed) { continue; } } - - if (target is Limb limb) + else if (target is Limb limb) { + if (limb.Removed) { continue; } position = limb.WorldPosition + Offset; } @@ -967,6 +984,8 @@ namespace Barotrauma foreach (ISerializableEntity target in targets) { + //if the effect has a duration, these will be done in the UpdateAll method + if (duration > 0) { break; } if (target == null) { continue; } foreach (Affliction affliction in Afflictions) { @@ -1047,13 +1066,22 @@ namespace Barotrauma Event ev = eventPrefab.CreateInstance(); if (ev == null) { continue; } eventManager.QueuedEvents.Enqueue(ev); - if (ev is ScriptedEvent scriptedEvent && !string.IsNullOrWhiteSpace(triggeredEventTag)) - { - List eventTargets = targets.Where(t => t is Entity).Cast().ToList(); - if (eventTargets.Any()) + if (ev is ScriptedEvent scriptedEvent) + { + if (!string.IsNullOrWhiteSpace(triggeredEventTargetTag)) { - scriptedEvent.Targets.Add(triggeredEventTag, eventTargets); + List eventTargets = targets.Where(t => t is Entity).Cast().ToList(); + + if (eventTargets.Any()) + { + scriptedEvent.Targets.Add(triggeredEventTargetTag, eventTargets); + } + } + + if (!string.IsNullOrWhiteSpace(triggeredEventEntityTag) && entity != null) + { + scriptedEvent.Targets.Add(triggeredEventEntityTag, new List { entity }); } } } @@ -1120,11 +1148,11 @@ namespace Barotrauma case ItemSpawnInfo.SpawnRotationType.MainLimb: rotation = user.AnimController.MainLimb.body.TransformedRotation; break; - default: + default: throw new NotImplementedException("Not implemented: " + itemSpawnInfo.RotationType); } rotation += MathHelper.ToRadians(itemSpawnInfo.Rotation * user.AnimController.Dir); - projectile.Shoot(user, sourceBody.SimPosition, sourceBody.SimPosition, rotation + spread, ignoredBodies: user.AnimController.Limbs.Where(l => !l.IsSevered).Select(l => l.body.FarseerBody).ToList(), createNetworkEvent: true); + projectile.Shoot(user, ConvertUnits.ToSimUnits(worldPos), ConvertUnits.ToSimUnits(worldPos), rotation + spread, ignoredBodies: user.AnimController.Limbs.Where(l => !l.IsSevered).Select(l => l.body.FarseerBody).ToList(), createNetworkEvent: true); } else { @@ -1135,25 +1163,18 @@ namespace Barotrauma break; case ItemSpawnInfo.SpawnPositionType.ThisInventory: { + Inventory inventory = null; if (entity is Character character && character.Inventory != null) { - int emptyCount = character.Inventory.Items.Count(it => it == null); - if (emptyCount - Entity.Spawner.CountSpawnQueue(spawnInfo => spawnInfo is EntitySpawner.ItemSpawnInfo itemSpawnInfo && itemSpawnInfo.Inventory == character.Inventory) > 0) - { - Entity.Spawner.AddToSpawnQueue(itemSpawnInfo.ItemPrefab, character.Inventory); - } + inventory = character.Inventory; } else if (entity is Item item) { - var inventory = item?.GetComponent()?.Inventory; - if (inventory != null) - { - int emptyCount = inventory.Items.Count(it => it == null); - if (emptyCount - Entity.Spawner.CountSpawnQueue(spawnInfo => spawnInfo is EntitySpawner.ItemSpawnInfo itemSpawnInfo && itemSpawnInfo.Inventory == inventory) > 0) - { - Entity.Spawner.AddToSpawnQueue(itemSpawnInfo.ItemPrefab, inventory); - } - } + inventory = item?.GetComponent()?.Inventory; + } + if (inventory != null && inventory.CanBePut(itemSpawnInfo.ItemPrefab)) + { + Entity.Spawner.AddToSpawnQueue(itemSpawnInfo.ItemPrefab, inventory, spawnIfInventoryFull: false); } } break; @@ -1170,12 +1191,13 @@ namespace Barotrauma } if (thisInventory != null) { - foreach (Item item in thisInventory.Items) + foreach (Item item in thisInventory.AllItems) { - if (item == null) continue; Inventory containedInventory = item.GetComponent()?.Inventory; - if (containedInventory == null || !containedInventory.Items.Any(i => i == null)) continue; - Entity.Spawner.AddToSpawnQueue(itemSpawnInfo.ItemPrefab, containedInventory); + if (containedInventory != null && containedInventory.CanBePut(itemSpawnInfo.ItemPrefab)) + { + Entity.Spawner.AddToSpawnQueue(itemSpawnInfo.ItemPrefab, containedInventory, spawnIfInventoryFull: false); + } break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs index 61f6dc39f..7622342b7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs @@ -108,7 +108,7 @@ namespace Barotrauma } //achievement for descending ridiculously deep - float realWorldDepth = Math.Abs(sub.Position.Y - Level.Loaded.Size.Y) * Physics.DisplayToRealWorldRatio; + float realWorldDepth = sub.RealWorldDepth; if (realWorldDepth > 5000.0f && Timing.TotalTime > GameMain.GameSession.RoundStartTime + 30.0f) { //all conscious characters inside the sub get an achievement @@ -380,7 +380,7 @@ namespace Barotrauma #endif var charactersInSub = Character.CharacterList.FindAll(c => !c.IsDead && - c.TeamID != Character.TeamType.FriendlyNPC && + c.TeamID != CharacterTeamType.FriendlyNPC && !(c.AIController is EnemyAIController) && (c.Submarine == gameSession.Submarine || (Level.Loaded?.EndOutpost != null && c.Submarine == Level.Loaded.EndOutpost))); diff --git a/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs b/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs index 6c844bb98..b9ae5a165 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs @@ -343,6 +343,14 @@ namespace Barotrauma } } + if (variableValue == null) + { + variableValue = "null"; +#if DEBUG + throw new ArgumentException($"Variable value \"{variableTag}\" was null."); +#endif + } + if (formatCapitals && !GameMain.Config.Language.Contains("Chinese")) { variableValue = HandleVariableCapitalization(text, variableTag, variableValue); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs index 60c134407..dc0173d3e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs @@ -85,17 +85,17 @@ namespace Barotrauma Categories.Add(this); } - public bool CanBeApplied(Item item, UpgradePrefab? upgradePrefab = null) + public bool CanBeApplied(Item item, UpgradePrefab? upgradePrefab) { if (IsWallUpgrade) { return false; } - if (upgradePrefab != null && item.disallowedUpgrades.Contains(upgradePrefab.Identifier)) { return false; } + if (upgradePrefab != null && upgradePrefab.IsDisallowed(item)) { return false; } return item.prefab.GetAllowedUpgrades().Contains(Identifier) || ItemTags.Any(tag => item.Prefab.Tags.Contains(tag) || item.Prefab.Identifier.Equals(tag, StringComparison.OrdinalIgnoreCase)); } - - public bool CanBeApplied(XElement element) + + public bool CanBeApplied(XElement element, UpgradePrefab prefab) { if (string.Equals("Structure", element.Name.ToString(), StringComparison.OrdinalIgnoreCase)) { return IsWallUpgrade; } @@ -105,6 +105,10 @@ namespace Barotrauma ItemPrefab? item = ItemPrefab.Find(null, identifier); if (item == null) { return false; } + string[] disallowedUpgrades = element.GetAttributeStringArray("disallowedupgrades", new string[0]); + + if (disallowedUpgrades.Any(s => s.Equals(Identifier, StringComparison.OrdinalIgnoreCase) || s.Equals(prefab.Identifier, StringComparison.OrdinalIgnoreCase))) { return false; } + return item.GetAllowedUpgrades().Contains(Identifier) || ItemTags.Any(tag => item.Tags.Contains(tag) || item.Identifier.Equals(tag, StringComparison.OrdinalIgnoreCase)); } @@ -243,6 +247,11 @@ namespace Barotrauma Prefabs.Add(this, isOverride); } + public bool IsDisallowed(Item item) + { + return item.disallowedUpgrades.Contains(Identifier); + } + public static UpgradePrefab? Find(string identifier) { return !string.IsNullOrWhiteSpace(identifier) ? Prefabs.Find(prefab => prefab.Identifier == identifier) : null; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/IdRemap.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/IdRemap.cs index 53919c70a..af7097579 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/IdRemap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/IdRemap.cs @@ -12,13 +12,13 @@ namespace Barotrauma private int maxId; - private List srcRanges; - private int destOffset; + private readonly List srcRanges; + private readonly int destOffset; public IdRemap(XElement parentElement, int offset) { destOffset = offset; - if (parentElement != null) + if (parentElement != null && parentElement.HasElements) { srcRanges = new List(); foreach (XElement subElement in parentElement.Elements()) @@ -26,7 +26,7 @@ namespace Barotrauma int id = subElement.GetAttributeInt("ID", -1); if (id > 0) { InsertId(id); } } - maxId = GetOffsetId(srcRanges.Last().Y + 1); + maxId = GetOffsetId(srcRanges.Last().Y) + 1; } else { @@ -42,7 +42,7 @@ namespace Barotrauma private void InsertId(int id) { - for (int i=0;i id) { @@ -66,10 +66,10 @@ namespace Barotrauma if (srcRanges[i].Y == (id - 1)) { srcRanges[i] = new Point(srcRanges[i].X, id); - if (i < (srcRanges.Count-1) && srcRanges[i].Y == srcRanges[i + 1].X) + if (i < (srcRanges.Count - 1) && srcRanges[i].Y == srcRanges[i + 1].X) { srcRanges[i] = new Point(srcRanges[i].X, srcRanges[i + 1].Y); - srcRanges.RemoveAt(i+1); + srcRanges.RemoveAt(i + 1); } return; } @@ -90,9 +90,9 @@ namespace Barotrauma if (srcRanges == null) { return (ushort)(id + destOffset); } int currOffset = destOffset; - for (int i=0;i= srcRanges[i].X && (id <= srcRanges[i].Y || (i == srcRanges.Count-1))) + if (id >= srcRanges[i].X && id <= srcRanges[i].Y) { return (ushort)(id - srcRanges[i].X + 1 + currOffset); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/RichTextData.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/RichTextData.cs index eb4fac829..07064ae46 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/RichTextData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/RichTextData.cs @@ -33,7 +33,7 @@ namespace Barotrauma int prevIndex = 0; int currIndex = 0; - for (int i=0;i filePaths = Directory.GetFileSystemEntries(enumPath).Select(s => Path.GetFileName(s)).ToList(); - if (filePaths.Any(s => s.Equals(subDirs[i], StringComparison.Ordinal))) + + string subDir = subDirs[i].TrimEnd(); + string enumPath = Path.Combine(startPath, filename); + + if (string.IsNullOrWhiteSpace(filename)) { - filename += subDirs[i]; + enumPath = string.IsNullOrWhiteSpace(startPath) ? "./" : startPath; + } + + List filePaths = Directory.GetFileSystemEntries(enumPath).Select(Path.GetFileName).ToList(); + + if (filePaths.Any(s => s.Equals(subDir, StringComparison.Ordinal))) + { + filename += subDir; } else { - IEnumerable correctedPaths = filePaths.Where(s => s.Equals(subDirs[i], StringComparison.OrdinalIgnoreCase)); + List correctedPaths = filePaths.Where(s => s.Equals(subDir, StringComparison.OrdinalIgnoreCase)).ToList(); if (correctedPaths.Any()) { corrected = true; @@ -152,7 +163,7 @@ namespace Barotrauma public static string RemoveInvalidFileNameChars(string fileName) { - var invalidChars = Path.GetInvalidFileNameChars().Concat(new char[] {':', ';'}); + var invalidChars = Path.GetInvalidFileNameChars().Concat(new char[] {':', ';', '<', '>', '"', '/', '\\', '|', '?', '*'}); foreach (char invalidChar in invalidChars) { fileName = fileName.Replace(invalidChar.ToString(), ""); @@ -572,7 +583,7 @@ namespace Barotrauma Process.Start(startInfo); } - public static string CleanUpPathCrossPlatform(this string path, bool correctFilenameCase = true) + public static string CleanUpPathCrossPlatform(this string path, bool correctFilenameCase = true, string directory = "") { if (string.IsNullOrEmpty(path)) { return ""; } @@ -584,7 +595,7 @@ namespace Barotrauma if (correctFilenameCase) { - string correctedPath = CorrectFilenameCase(path, out _); + string correctedPath = CorrectFilenameCase(path, out _, directory); if (!string.IsNullOrEmpty(correctedPath)) { path = correctedPath; } } diff --git a/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub b/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub index ea738a58e..3e6ffa14a 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 fb6c5223f..7111e046d 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 69cf3a60b..92b63ca88 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Dugong.sub and b/Barotrauma/BarotraumaShared/Submarines/Dugong.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Hemulen.sub b/Barotrauma/BarotraumaShared/Submarines/Hemulen.sub index 92d0a7813..d307e0ca5 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Hemulen.sub and b/Barotrauma/BarotraumaShared/Submarines/Hemulen.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Humpback.sub b/Barotrauma/BarotraumaShared/Submarines/Humpback.sub index 176e8332c..f18f2889c 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 3cd744805..f59877ef3 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub and b/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub b/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub index 5a69da02f..6bf6eb494 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub and b/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Orca.sub b/Barotrauma/BarotraumaShared/Submarines/Orca.sub index dea22be13..901e9decc 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 d7d09c724..d3f62f6fd 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 5b2a755e7..5637a8ce6 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Remora.sub and b/Barotrauma/BarotraumaShared/Submarines/Remora.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub b/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub index f27a05fe8..539211663 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub and b/Barotrauma/BarotraumaShared/Submarines/RemoraDrone.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Selkie.sub b/Barotrauma/BarotraumaShared/Submarines/Selkie.sub index 4840c9d73..c989a3fa9 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Selkie.sub and b/Barotrauma/BarotraumaShared/Submarines/Selkie.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Typhon.sub b/Barotrauma/BarotraumaShared/Submarines/Typhon.sub index 3d60b8f12..c3cda313c 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 cf91783c4..179ad01de 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub and b/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Venture.sub b/Barotrauma/BarotraumaShared/Submarines/Venture.sub index 97d16a3a3..f014bdfec 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Venture.sub and b/Barotrauma/BarotraumaShared/Submarines/Venture.sub differ diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index ffc6ffd72..ef885bf34 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,269 @@ +--------------------------------------------------------------------------------------------------------- +v0.12.0.2 +--------------------------------------------------------------------------------------------------------- + +- Fixed console errors when hatchlings spawn. +- Fixed beds causing injuries instead of healing them. +- Fixed diving suit holder not filling up oxygen tanks. +- Fixed messed up frozen seed display sprite. + +--------------------------------------------------------------------------------------------------------- +v0.12.0.1 +--------------------------------------------------------------------------------------------------------- + +- Adjustments and balancing to monster spawns. +- Modifed meds, buffs and poisons fabrication times. +- Potential fix to occasional disconnects with an "index was outside the bounds of the array (ENTITY_POSITION)" error message. Happened when lots of items and characters were being created and removed in rapid succession, for example when using turrets against large numbers of enemies. +- Fixed a rare crash caused by an "index out of range" exception in Hull.Update after loading or mirroring certain custom submarines. +- Fixed submarine class not affecting the depth at which a submarine starts taking pressure damage. +- Linked subs inherit the submarine class of the parent sub. Fixes drones/shuttles on deep diver subs getting crushed in the late game levels. +- Fixed nav terminal displaying pressure warnings when the sub is past the crush depth of a non-upgraded sub. +- Fixed character variants not having inventories. +- Fixed monsters sometimes spawning inside floating ice chunks. +- Logbooks now longer spawn in secure steel cabinets in wreck missions. +- Crystal and rock caves can appear in cold caverns. +- Show cave entrance markers on the sonar during mineral missions. +- Allow using some the new client-side console commands without command permissions in multiplayer. +- Fixed empty container indicators being animated on nearly all item. +- Fixed incorrect contained state indicator on flashlights. +- Display target item name in the tooltip for orders which have no options but have target item icons. +- Decreased the amount of burn on first patient in medic tutorial. +- Removed the deals groups from the store interface when there are no deals set for the current location. +- Fixed wrecks sometimes blocking cave entrances. +- Fixed Kastrull drone flooding when undocking using the button next to the hatch. +- Fixed wire/component placement grid having a too high opacity. +- Fixed bots being unable to shoot ice spires if the point on the spire that's closest to the turret is outside the turret's rotation limits. +- Fixed ancient weapon being sometimes unable to damage ruin walls. + +--------------------------------------------------------------------------------------------------------- +v0.12.0.0 +--------------------------------------------------------------------------------------------------------- + +Cave improvements: +- New cave types and improvements to cave sprites. +- New cave hazards: exploding mushrooms, gas vents that drain oxygen and fuel tanks, sharp crystals that inflict bleeding and lacerations, hallucination-inducing plants. +- Made the caves more narrow. +- Position caves and ruins closer to the main path so the players won't have to swim hundreds of meters just to get to the entrance. +- Completing a nest mission turns adjacent empty locations to "Explored". +- Monster spawns are delayed up to a certain limit when most of the human players are exploring a ruin, wreck or a cave. +- Show a sonar marker at the cave's entrance in nest missions. +- Added some nest-specific level objects. + +Additions and changes: +- Implemented stacking small items. +- Decreased toolbelt capacity to 6 slots. +- Rebalanced level events to have a better difficulty curve. +- Further improvements and balancing to the way locations change to other types of locations. +- The owner of a duffel bag is shown when hovering the cursor over one. +- Increased vanilla drones' range to 400 m. +- Nerfed stunning. The effect is now incremental: for example, a few hits from a stun baton only slows the target down, but a full stun requires several hits. +- Allow laying on beds. +- Added mudraptor, thresher and crawler hatchlings. +- Removed vision obstructing effects from diving mask and diving suit. +- Added "quickstart" console command. +- Added "setfreecamspeed" console command. +- Zoom more slowly when holding Ctrl in freecam. The speed can be adjusted with the "camerasettings" command. +- Added "CauseSpeechImpediment" attribute to afflictions that stops huskaffliction from causing a speech impediment when set to false. +- Added "sendmessages" attribute to affliction prefabs. When set to false it prevents husk affliction from sending messages on the screen. +- Increased the steps in the aim assist slider to make it possible to set it more precisely (there's a big difference between 0% and 10%). +- Show a "waiting for the campaign to start" text for clients without campaign management permissions in the game mode panel when campaign is being set up. +- Disable all the non-campaign-related elements in the server lobby when a campaign or campaign setup is active. +- Added some diving suits to outpost modules. +- Made engine's propeller damage indicator in the sub editor match the damage radius. +- Invert nav terminal velocity_x output and applied engine force for flipped subs. This should make it so that the sub will go forward when the engine is in "forward" state, instead of going to the right which is the default forward direction. +- Made cabinets waterproof. +- Nav terminals display a warning then the autopilot detects an ice spire. +- Modified oxygen tank, welding fuel tank, underwater scooter and duffel bag densities (they don't sink or ascend as quickly as they used to). +- Reduced skill requirements for rewiring. +- Restrict Concat Component's output length. +- Welding tools can now be used to destroy ballast flora. +- Made floating ice chunks easier to destroy. +- Ballast flora can be damaged by explosions and tools even when it's submerged. +- Select the items/structures in an assembly when placing one in the sub editor. +- Fertilizer no longer increases water consumption on plants. +- Replaced fuel rods for uranium in fertilizer recipes. +- Allow adjusting sound volumes at 1% increments. +- Allow opening Command Interface's manual assignment when using hotkeys or "clickless" mode. Can be done by holding Shift (same key used to open the contextual interface) when navigating to the assignment phase. +- Added new editable property "NonPlayerTeamInteractable" for Items. Prevents players or the bots in their crew from interacting with the item, but still allows outpost NPCs to interact with it. +- Made outpost reactors non-interactable for player team characters. +- Monsters can spawn at both sides of the level during PvP missions, as opposed to always spawning near the sub on the left. +- Disable trying to edit human heads in the character editor, because it would break. +- Monsters can now use doors for fleeing and they should be able to break doors that are in the way to the escape target. However, if the target they are fleeing from is in the same room, they still "panic" and just try to run away from the target. +- Previously the swimming speed of monsters was halved. Now it's clamped. This fixes slow monsters like husk swimming really slow inside the submarine, but it also affects the others. +- Added a diving knife in one of the security item sets, so that the security officers always have a lethal weapon and won't have to keep bashing the creatures with a stun baton. +- Added a medic crate for medical items that are bought from an outpost. Previously the medical items were spawned inside a chemical crate. +- Changed the sounds heard in psychosis depending on whether the character is inside or outside of a submarine. +- Husks can now operate doors and hatches. They can also climb ladders and break doors, which they can't open. +- Added ability to set custom background images in sub editor. +- Added a checkbox to skip localization to item labels. +- Made changes to ignore icon visibility: they can now be seen from further and won't fade out when you move close to them. +- Made item selling prices adjust based on the store balance: as the store balance decreases, the store will offer less for the items you are selling. +- Added "wikiimage_character" command, which generates an isolated image of the currently controlled character. +- Added "wikiimage_sub" command, which generates an isolated image of the currently loaded submarine. +- Renamed memory component's "signal_store" input as "lock_state". +- Added visual snap grid into submarine editor and "togglegrid" console command to toggle it. +- Relay's toggle input ignores "0" signals. +- Only allow attaching one wire to a reactor's power_out connection, because more leads to power issues. +- Made motion sensor's IgnoreDead, Offset and Target properties editable in-game. +- Entering the sub while docked to an outpost doesn't make the music switch from an outpost track to a normal one. +- Restricted pet name tag to 32 characters. +- Instead of a static starting wallet of 2500 marks, the players get 8500 minus the price of the starting sub. +- Replaced existing skill progression feedback with skill increase popups. +- Added new music track, "Extraterrestrial Broadcast". +- Fruits now float on water. +- Added a HSV color picker in sub editor that can be accessed by clicking the small color preview rectangle in the property editor. +- Ctrl+V keybind in sub editor now pastes on the position of your cursor instead of at the center of the screen. +- Holding Alt and dragging the mouse now brings up a measuring tape in sub editor. +- Added "bindkey", "unbindkey" and "savebinds" console commands that can be used to assign console commands to keys. +- Disabled hotbar slot keybinds when holding the Windows Key on Linux. +- Allow banning players who've left the server by clicking on their name in the crew list or server log. +- Husks now ignore incapacitated characters. They also know how to crouch to reach stunned/ragdolled characters lying on the floor. +- Improved the waypoint generator. +- Added store-specific Daily Specials and Requested Goods that change as the game progresses: Daily Specials are cheaper to buy and Requested Goods are more profitable to sell. +- Added store-specific price variance: every store has its own small price factor that affects all item prices. +- Hammerhead spawns are no longer afraid of being shot with sub's turrets. They also try to get in more aggressively. +- Readjusted the flame particle positions on the welding tool and the plasma cutter. +- Lowered the electrical skill required for wiring oxygen tank shelves and diving suit lockers. +- Mission cargo items are now marked with the red hand item. Taking them is considered stealing. +- Added glowsticks. +- Added an option to change the text scale. + +Bots: +- Bots report ballast flora when they see it. +- Bots operating turrets now report what they see. They also call "firing" a bit less frequently. +- Bots using a coilgun/railgun report which kind of enemies they see. +- Fixed medics sometimes not letting go of the character they're treating after running out of medical items. +- Fixed medics not ignoring targets that they can't heal, because they don't have the items for it, which caused the medics to be trapped in a looping behavior. +- Outpost NPCs can arrest or kill players who try to cause leaks in the outpost. +- Changes to how the bots operate repair tools, so that they won't fail so easily when the water forces kick in. +- Bots (only the crew) now tolerate a bit more damage from repair tools before reacting. +- Bots should now properly respect ignored targets also when they are already targeting the item when you tell them to ignore it. +- Fixed bots not equipping diving gear when the oxygen level is low and there is no leaks/water in the hull, causing them to suffocate. +- Fixed bots "forgetting" autonomous operate (operate reactor or steer) orders if the objectives happen to fail. +- Fixed bots sometimes incorrectly abandoning a movement objective, when the path requires a diving gear. +- Bots should now know how to get back inside through gaps in the hull. +- Fixed NPCs not being able to repower the reactor if the player somehow manages to unpower it. +- Bots now run when they are ordered to clean up things. +- Bots now ignore mudraptor eggs (to not foil your evil plans by cleaning them up). +- AI Pathing: Fixed a huge penalty given to stairs causing characters to choose weird paths to avoid stairs. +- Fixed bots running towards a door when they can't find a path while following a target, which was not the intended behavior. The bots should now stop moving and wait in place instead. +- Fixed docking port not always being properly connected to a door, causing a missing link between the waypoints and making it impossible for the bots to access the port on some subs. The accepted distance is now relative to the docking port's sprite's height so that all doors inside the sprite should be treated as close enough. Additionally you can manually link a door to a docking port in the sub editor. +- Fixed bots thinking that they need a diving suit to access certain drones/airlocks on subs that the creator haven't manually removed the waypoints on the ladders outside the airlock door. There's no need to remove those waypoints anymore, although it shouldn't matter if you do. +- Bots should now always heal players when ordered to. They still use a threshold for targeting other bots, but it's now slightly higher (90 instead of 85). This only applies when the bots are ordered, not when they act on their own. +- Bots now speak about not having any targets after a 3 sec delay. Fixes bots sometimes complaining that there's no targets even when there are. +- Fixed bots not properly ignoring non-interactable containers. +- Fixed bots dropping off from ladders when they first try to go up using a ladder and then down using another ladder right next to the first ladder. Happened especially in Kastrull. +- Fixed bots taking items from mission cargo containers (e.g. clean up or rescue). +- Fixed bots not properly checking the line of sight while trying to extinguish fires. +- Bots don't anymore drop empty oxygen tanks in the sea, if they have room in the inventory. +- Bots should now take oxygen tanks with less than 80% either to an oxygen generator or an oxygen tank shelf. Secondary places for non-empty tanks are the diving and the supply cabinets. +- Bots should now take battery cells with less than 80% to a battery or charging dock (added "batterycellrecharger" tag for these items). +- Bots don't yell anymore that they need more oxygen when they are running out of oxygen and have a non-empty oxygen tank(s) in their inventory. +- Fixed bots not respecting the Wait order inside ruins. +- Bots now shoot ice spires when the sub is moving towards them or if they are very close to the turret. +- Bots steering the sub now report ice spires they spot on the sonar. +- Bots now switch batteries when operating scooters. They also unequip the scooter if they run out of batteries, instead of just swimming with it. +- Bots now know how to switch batteries to the stun baton. +- Bots don't drop empty welding tanks or ammunition in the sea anymore, if they have room in the inventory. +- Fixed bots not shooting through turrets that block the line of sight. + +Modding: +- Fixed mods not being sorted correctly until changing the order through the settings. +- Fixed mod load order not being restored correctly when leaving a server. +- Status effect definition attributes are now case insensitive. +- TriggerEvent in StatusEffects now supports inline events and the event tags have been renamed to "statuseffecttarget" and "statuseffectentity". +- Added support for defining a contextual name for an order (e.g. "Wait" and "Wait Here"). +- Added support for IntegerInput elements (with "min" and "max" attributes) for the CustomInterface component. +- Option to define character variants that override some parts of another character. See the "Content/Characters/Variants" folder for some usage examples. +- Fixed a number of crashes in custom repair tools, especially if they operate on their own without requiring a user. +- Added "needsair" attribute to husk affliction, when set to true it disables low oxygen resistance granted by husk affliction. +- Fixed crashing when a StatusEffect targets a removed limb. +- Fixed conditional "And" comparisons not always working correctly when targeting items (the conditional needed to be true on the item and all it's components). +- Added support for overriding textures on character variants. Only supports overriding the entire texture, not separate textures per limb. +- Added support for overriding animations on character variants. +- Added min and max conditions (in percentages) for preferred containers. +- Added support for randomized deconstruction output: Deconstruct elements now support "chooserandom" and "amount" attributes, Item elements support "commonness" attribute. +- Added ClearTagAction that removes the specified tag from the event. +- Added CheckAfflictionAction that can be used to check if a character has an affliction. +- Added "InWater" property to characters. +- Planter component now triggers "OnPicked" status effects when interacted with. +- Allow items with a kinematic physics body to have a connection panel component. +- Fixed HasStatusTag conditionals only checking the first active status effect. +- Fixed multiple characters spawning when a character infected with multiple different types of husk afflictions dies. + +Bugfixes: +- Fixed desync when entering a new level when there's a logbook with a very long message in the sub or in someone's inventory in the multiplayer campaign. +- Fixed very large numbers of submarines in the "Submarines/Downloaded/" folder causing excessive loading times, freezes and high memory usage. +- Delete submarines from Submarines/Downloaded when launching the game and hide downloaded subs from menus. The folder is now essentially a temporary folder for storing a server's submarines during a game session, not a place for persistent storage. +- Fixed crashing when trying to "select matching items" when right-clicking a linked sub in the sub editor. +- Fixed filename case issues and using backslashes instead of forward slashes in mod file paths causing errors on Linux and Mac. +- Fixed crashing when trying to respawn a bot in the multiplayer campaign. +- Fixed server list only showing up to 50 servers. +- Fixed connections that have the same start and end location sometimes getting generated on the campaign map, leading to a crash when trying to load the campaign save. +- Number input boxes don't clamp the value until the input box loses focus or enter is pressed. Fixes clamped values being very difficult to edit. +- Fixed a crash in GetDisguisedSprites. +- Mission events unlock the mission even if the conversation gets interrupted. +- Fixed lights on turrets rotating around the origin of the item, not the origin of the barrel. +- Fixed nav terminal's "velocity_in" input doing nothing. +- Fixed newly placed coilguns and searchlights not rotating the light sprite in sub editor. +- Fixed R-29 cargo lights not toggling on. +- Fixed missing waypoints in Berilia. +- Fixed docking ports' DockingDistance not being affected by the scale of the item. +- Fixed KarmaManager's MaxStructureDamageKarmaDecreasePerSecond not working as intended, allowing karma to decrease more than it should when doing lots of structure damage in a short time (e.g. when using explosives). +- Fixed engine's propeller hitbox not being moved or scaled when the engine is rescaled. +- Fixed holes in walls becoming non-see-through when starting a new round in the campaign. +- Fixed corrupted/invalid content packages whose xml file can't be loaded being added to the list of content packages, leading to a crash. +- Fixed devices failing to receive power if connected directly to a docking port's power connection without a junction box or relay in between. +- Fixed "disallowed upgrades" field not being taken into account in the upgrade UI when using prefab identifiers. +- Fixed level generation sometimes failing to generate the complete path to the destination. +- Fixes to docked subs being placed on the wrong side of the docking target in some cases (namely, when neither of the ports is connected to a door). Also added "ForceDockingDirection" setting to docking ports to enforce the direction if the automatic logic still fails due to some weird port setup. +- Fixed "velocity invalid" error if a monster that's indoors eats a corpse that's outdoors or vice versa (e.g. if you drag a corpse through the airlock while a monster is eating it). +- Fixed diving suit not giving any protection for radiation sickness. +- Fixed conversation prompts not disappearing if the controlled character dies when executing subactions (for instance, when the clowns are approaching the players at the beginning of the "clown relations" event). +- Fixed crashing when taking control of a black moloch with console commands. +- Fixed character editor creating tiny limbs if the mouse cursor goes outside of the sprite sheet area when the user is drawing a new limb. Drawing new limbs is no longer restricted to the sprite sheet area (it still works in display screen space!). +- Fixed husks and humanhusks targeting turrets when they are inside the submarine. +- Fixed monsters sometimes choosing weird paths when the hulls are flooding. +- Fixed assignment tooltip sometimes being displayed on command interface when it shouldn't be. +- Fixed ability to open manual assignment for orders targeting all characters. +- Fixed player characters' orders not being reset in between multiplayer rounds. +- Fixed "unignore" icons being displayed in multiplayer. +- Fixed crew list background blocking mouse input. +- Fixed order icons being always displayed over container when the order target is contained. +- Fixed CustomInterface component's UI text not reflecting the actual signal when changed by another player in multiplayer. +- Fixed inconsistencies in meds when fired from a syringe gun. +- Fixed thalamus spawning too many leucocytes in wrecks (particularly on higher difficulties). +- Fixed ability to choose a campaign save filename that's illegal on Windows when hosting a server on Linux or Mac, preventing Windows players from joining the server. +- Fixed workshop item download prompt not fitting on the screen when trying to join a server that has a large number of mods installed. +- Fixed being able to hear ready check ticking when the popup wasn't visible. +- Fixed an occasional lag following a crash when there's Spinelings present in the game (#4453). +- Fixed autopilot trying to steer away from connected subs that aren't directly docked to the main sub (e.g. drone docked to a drone docked to the main sub). +- Fixed a crash in the character editor when editing a limb's source with "adjust collider" enabled and if the source rect's size is zero or negative. +- Fixed docking ports not locking client-side if hulls fail to generate between the ports (e.g. if the thing docking to the sub doesn't have hulls). +- Fixed subs with a tall shuttle docked on top sometimes spawning partially inside the level's top wall. +- Fixed melee weapons being able to hit through walls/doors as long as the origin of the item is on the same side of the wall/door as the user. +- Fixed ballast flora jamming doors permanently if it dies while trying to drown a player in the ballast tank. +- Fixed pets not spawning at the position of the owner. +- Fixed rare "failed to generate a wall (not enough vertices)" error when generating a level. +- Fixed ability to see inside secure steel crates without appropriate access by swapping them with a normal crate. +- Fixed welded doors staying stuck and nav terminal not getting reset when redispatching a shuttle. +- Fixed mudraptor shells spawning outside the mudraptor's inventory. +- Fixed recovered shuttles disappearing if you quit during the round immediately after purchasing shuttle recovery in multiplayer campaign. +- Fixed logbooks always spawning in the same cabinet in a given wreck. +- Fixed items with a physics body disappearing when they are outside hulls during saving. +- Fixed items attached to walls (e.g. signal components) deattaching when resetting them to prefab values in the sub editor. +- Fixed characters that have been turned invisible by psychosis staying invisible when you switch to them. +- Fixed crashing when selecting a client who's controlling a monster in the tab menu. +- Fixed "where no man has gone before" achievement being impossible to unlock due to the depth being calculated incorrectly. +- Fixed submarine switching deleting all upgrades in multiplayer. +- Fixed Hammerhead Matriarch often not being able to break the sub's walls when hitting it. +- Fixed Hammerhead Matriarch's head deforming incorrectly (this fix may affect any modded content using conditional sprites with deformations). +- Fixed crash in sub editor when copying a linked submarine that you lacked the original file to. +- Fixed turrets and searchlights having incorrect light rotation when minimum rotation limit was the same as maximum rotation limit. +- Fixed popup messages preventing you from steering on the navigation terminal. +- Fixed inability to start a server when there's a very large number of subs installed. +- Fixed XP messages flying away when inside a moving sub. + --------------------------------------------------------------------------------------------------------- v0.11.0.10 --------------------------------------------------------------------------------------------------------- diff --git a/Barotrauma/BarotraumaShared/config.xml b/Barotrauma/BarotraumaShared/config.xml index 9baa48f85..62d4cca5c 100644 --- a/Barotrauma/BarotraumaShared/config.xml +++ b/Barotrauma/BarotraumaShared/config.xml @@ -43,6 +43,8 @@ Grab="G" Shoot="PrimaryMouse" Deselect="SecondaryMouse" + TakeOneFromInventorySlot="LeftControl" + TakeHalfFromInventorySlot="LeftShift" SelectPreviousCharacter="Z" SelectNextCharacter="X" /> diff --git a/Libraries/Facepunch.Steamworks/Structs/LobbyQuery.cs b/Libraries/Facepunch.Steamworks/Structs/LobbyQuery.cs index f17ecf865..1401e5820 100644 --- a/Libraries/Facepunch.Steamworks/Structs/LobbyQuery.cs +++ b/Libraries/Facepunch.Steamworks/Structs/LobbyQuery.cs @@ -43,21 +43,44 @@ namespace Steamworks.Data #region String key/value filter internal Dictionary stringFilters; + + internal Dictionary> stringFiltersExclude; + /// /// Filter by specified key/value pair; string parameters /// - public LobbyQuery WithKeyValue( string key, string value ) + public LobbyQuery WithKeyValue(string key, string value) { - if ( string.IsNullOrEmpty( key ) ) - throw new System.ArgumentException( "Key string provided for LobbyQuery filter is null or empty", nameof( key ) ); + if (string.IsNullOrEmpty(key)) + throw new System.ArgumentException("Key string provided for LobbyQuery filter is null or empty", nameof(key)); - if ( key.Length > SteamMatchmaking.MaxLobbyKeyLength ) - throw new System.ArgumentException( $"Key length is longer than {SteamMatchmaking.MaxLobbyKeyLength}", nameof( key ) ); + if (key.Length > SteamMatchmaking.MaxLobbyKeyLength) + throw new System.ArgumentException($"Key length is longer than {SteamMatchmaking.MaxLobbyKeyLength}", nameof(key)); - if ( stringFilters == null ) + if (stringFilters == null) stringFilters = new Dictionary(); - stringFilters.Add( key, value ); + stringFilters.Add(key, value); + + return this; + } + + public LobbyQuery WithoutKeyValue(string key, string value) + { + if (string.IsNullOrEmpty(key)) + throw new System.ArgumentException("Key string provided for LobbyQuery filter is null or empty", nameof(key)); + + if (key.Length > SteamMatchmaking.MaxLobbyKeyLength) + throw new System.ArgumentException($"Key length is longer than {SteamMatchmaking.MaxLobbyKeyLength}", nameof(key)); + + if (stringFiltersExclude == null) + stringFiltersExclude = new Dictionary>(); + + if (!stringFiltersExclude.ContainsKey(key)) + { + stringFiltersExclude.Add(key, new List()); + } + stringFiltersExclude[key].Add(value); return this; } @@ -184,20 +207,26 @@ namespace Steamworks.Data SteamMatchmaking.Internal.AddRequestLobbyListFilterSlotsAvailable( slotsAvailable.Value ); } - if ( maxResults.HasValue ) + if (stringFilters != null) { - SteamMatchmaking.Internal.AddRequestLobbyListResultCountFilter( maxResults.Value ); - } - - if ( stringFilters != null ) - { - foreach ( var k in stringFilters ) + foreach (var k in stringFilters) { - SteamMatchmaking.Internal.AddRequestLobbyListStringFilter( k.Key, k.Value, LobbyComparison.Equal ); + SteamMatchmaking.Internal.AddRequestLobbyListStringFilter(k.Key, k.Value, LobbyComparison.Equal); } } - if( numericalFilters != null ) + if (stringFiltersExclude != null) + { + foreach (var k in stringFiltersExclude) + { + foreach (var v in k.Value) + { + SteamMatchmaking.Internal.AddRequestLobbyListStringFilter(k.Key, v, LobbyComparison.NotEqual); + } + } + } + + if ( numericalFilters != null ) { foreach ( var n in numericalFilters ) { @@ -212,6 +241,11 @@ namespace Steamworks.Data SteamMatchmaking.Internal.AddRequestLobbyListNearValueFilter( v.Key, v.Value ); } } + + if (maxResults.HasValue) + { + SteamMatchmaking.Internal.AddRequestLobbyListResultCountFilter(maxResults.Value); + } } /// diff --git a/Libraries/OpenAL-Soft/oal_soft.diff b/Libraries/OpenAL-Soft/oal_soft.diff index b43055ca1..27f2f8728 100644 --- a/Libraries/OpenAL-Soft/oal_soft.diff +++ b/Libraries/OpenAL-Soft/oal_soft.diff @@ -163,7 +163,7 @@ index 30d363af..f759a484 100644 return; } diff --git a/alc/backends/wasapi.cpp b/alc/backends/wasapi.cpp -index 8e43aa7c..0f313dea 100644 +index 8e43aa7c..45950062 100644 --- a/alc/backends/wasapi.cpp +++ b/alc/backends/wasapi.cpp @@ -54,6 +54,12 @@ @@ -356,15 +356,24 @@ index 8e43aa7c..0f313dea 100644 ERR("Failed to reactivate audio client: 0x%08lx\n", hr); return hr; } -@@ -1521,6 +1548,7 @@ HRESULT WasapiCapture::resetProxy() +@@ -1521,8 +1548,14 @@ HRESULT WasapiCapture::resetProxy() hr = mClient->IsFormatSupported(AUDCLNT_SHAREMODE_SHARED, &OutputType.Format, &wfx); if(FAILED(hr)) { -+ alcCallErrorReasonCallback(std::string("WASAPI capture proxy reset failed: failed to check format support (HRESULT ")+toStringHex(hr)+")"); - ERR("Failed to check format support: 0x%08lx\n", hr); - return hr; +- ERR("Failed to check format support: 0x%08lx\n", hr); +- return hr; ++ alcCallErrorReasonCallback(std::string("WASAPI capture proxy reset error: failed to check format support (HRESULT ")+toStringHex(hr)+")"); ++ hr = mClient->GetMixFormat(&wfx); ++ if (FAILED(hr)) ++ { ++ alcCallErrorReasonCallback(std::string("WASAPI capture proxy reset failed: failed to get mix format (HRESULT ")+toStringHex(hr)+")"); ++ ERR("Failed to check format support: 0x%08lx\n", hr); ++ return hr; ++ } } -@@ -1619,6 +1647,7 @@ HRESULT WasapiCapture::resetProxy() + + mSampleConv = nullptr; +@@ -1619,6 +1652,7 @@ HRESULT WasapiCapture::resetProxy() buf_time.count(), 0, &OutputType.Format, nullptr); if(FAILED(hr)) { @@ -372,7 +381,7 @@ index 8e43aa7c..0f313dea 100644 ERR("Failed to initialize audio client: 0x%08lx\n", hr); return hr; } -@@ -1630,6 +1659,7 @@ HRESULT WasapiCapture::resetProxy() +@@ -1630,6 +1664,7 @@ HRESULT WasapiCapture::resetProxy() hr = mClient->GetBufferSize(&buffer_len); if(FAILED(hr)) { @@ -380,7 +389,7 @@ index 8e43aa7c..0f313dea 100644 ERR("Failed to get buffer size: 0x%08lx\n", hr); return hr; } -@@ -1641,6 +1671,7 @@ HRESULT WasapiCapture::resetProxy() +@@ -1641,6 +1676,7 @@ HRESULT WasapiCapture::resetProxy() hr = mClient->SetEventHandle(mNotifyEvent); if(FAILED(hr)) { @@ -388,7 +397,7 @@ index 8e43aa7c..0f313dea 100644 ERR("Failed to set event handle: 0x%08lx\n", hr); return hr; } -@@ -1653,7 +1684,10 @@ void WasapiCapture::start() +@@ -1653,7 +1689,10 @@ void WasapiCapture::start() { const HRESULT hr{pushMessage(MsgType::StartDevice).get()}; if(FAILED(hr)) @@ -399,7 +408,7 @@ index 8e43aa7c..0f313dea 100644 } HRESULT WasapiCapture::startProxy() -@@ -1663,6 +1697,7 @@ HRESULT WasapiCapture::startProxy() +@@ -1663,6 +1702,7 @@ HRESULT WasapiCapture::startProxy() HRESULT hr{mClient->Start()}; if(FAILED(hr)) { @@ -407,7 +416,7 @@ index 8e43aa7c..0f313dea 100644 ERR("Failed to start audio client: 0x%08lx\n", hr); return hr; } -@@ -1676,9 +1711,17 @@ HRESULT WasapiCapture::startProxy() +@@ -1676,9 +1716,17 @@ HRESULT WasapiCapture::startProxy() mKillNow.store(false, std::memory_order_release); mThread = std::thread{std::mem_fn(&WasapiCapture::recordProc), this}; } @@ -425,7 +434,7 @@ index 8e43aa7c..0f313dea 100644 ERR("Failed to start thread\n"); hr = E_FAIL; } -@@ -1737,6 +1780,12 @@ bool WasapiBackendFactory::init() +@@ -1737,6 +1785,12 @@ bool WasapiBackendFactory::init() InitResult = future.get(); } catch(...) { diff --git a/Libraries/XNATypes/Rectangle.cs b/Libraries/XNATypes/Rectangle.cs index c9c7b77a2..7929ec0d7 100644 --- a/Libraries/XNATypes/Rectangle.cs +++ b/Libraries/XNATypes/Rectangle.cs @@ -526,6 +526,29 @@ namespace Microsoft.Xna.Framework result.Width = Math.Max(value1.Right, value2.Right) - result.X; result.Height = Math.Max(value1.Bottom, value2.Bottom) - result.Y; } + + public void AddPoint(Point point) + { + if (point.X < X) + { + Width += X - point.X; + X = point.X; + } + else if (point.X > Right) + { + Width += point.X - Right; + } + + if (point.Y < Y) + { + Height += Y - point.Y; + Y = point.Y; + } + else if (point.Y > Bottom) + { + Height += point.Y - Bottom; + } + } #endregion } diff --git a/README.md b/README.md index 871417b86..90ea03182 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ If you're interested in working on the code, either to develop mods or to contri **Forums:** http://undertowgames.com/forum/ -**Wiki:** http://barotrauma.gamepedia.com/Barotrauma_Wiki +**Wiki:** https://barotraumagame.com/wiki/Main_Page ## Prerequisities: ### Windows