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/AITarget.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/AITarget.cs index a1be8d950..cdf6ce5f5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/AITarget.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/AITarget.cs @@ -11,6 +11,9 @@ namespace Barotrauma { if (!ShowAITargets) { return; } var pos = new Vector2(WorldPosition.X, -WorldPosition.Y); + float thickness = 1 / Screen.Selected.Cam.Zoom; + + float offset = MathUtils.VectorToAngle(new Vector2(sectorDir.X, -sectorDir.Y)) - (sectorRad / 2f); if (soundRange > 0.0f) { Color color; @@ -26,8 +29,16 @@ namespace Barotrauma { color = Color.OrangeRed; } - ShapeExtensions.DrawCircle(spriteBatch, pos, SoundRange, 100, color, thickness: 1 / Screen.Selected.Cam.Zoom); - ShapeExtensions.DrawCircle(spriteBatch, pos, 3, 8, color, thickness: 2 / Screen.Selected.Cam.Zoom); + + if (sectorRad < MathHelper.TwoPi) + { + spriteBatch.DrawSector(pos, SoundRange, sectorRad, 100, color, offset: offset, thickness: thickness); + } + else + { + spriteBatch.DrawCircle(pos, SoundRange, 100, color, thickness: thickness); + } + spriteBatch.DrawCircle(pos, 3, 8, color, thickness: 2 / Screen.Selected.Cam.Zoom); GUI.DrawLine(spriteBatch, pos, pos + Vector2.UnitY * SoundRange, color, width: (int)(1 / Screen.Selected.Cam.Zoom) + 1); } if (sightRange > 0.0f) @@ -47,7 +58,14 @@ namespace Barotrauma // disable the indicators for structures and hulls, because they clutter the debug view return; } - ShapeExtensions.DrawCircle(spriteBatch, pos, SightRange, 100, color, thickness: 1 / Screen.Selected.Cam.Zoom); + if (sectorRad < MathHelper.TwoPi) + { + spriteBatch.DrawSector(pos, SightRange, sectorRad, 100, color, offset: offset, thickness: thickness); + } + else + { + spriteBatch.DrawCircle(pos, SightRange, 100, color, thickness: thickness); + } ShapeExtensions.DrawCircle(spriteBatch, pos, 6, 8, color, thickness: 2 / Screen.Selected.Cam.Zoom); GUI.DrawLine(spriteBatch, pos, pos + Vector2.UnitY * SightRange, color, width: (int)(1 / Screen.Selected.Cam.Zoom) + 1); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs index dd836a3c7..42ffa3a40 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs @@ -1,22 +1,12 @@ using Microsoft.Xna.Framework; using FarseerPhysics; +using System; +using System.Linq; namespace Barotrauma { partial class HumanAIController : AIController { - public static bool debugai; - - partial void InitProjSpecific() - { - /*if (GameMain.GameSession != null && GameMain.GameSession.CrewManager != null) - { - CurrentOrder = Order.GetPrefab("dismissed"); - objectiveManager.SetOrder(CurrentOrder, "", null); - GameMain.GameSession.CrewManager.SetCharacterOrder(Character, CurrentOrder, null, null); - }*/ - } - public override void DebugDraw(Microsoft.Xna.Framework.Graphics.SpriteBatch spriteBatch) { if (Character == Character.Controlled) { return; } @@ -24,6 +14,7 @@ namespace Barotrauma Vector2 pos = Character.WorldPosition; pos.Y = -pos.Y; Vector2 textOffset = new Vector2(-40, -160); + textOffset.Y -= Math.Max(ObjectiveManager.CurrentOrders.Count - 1, 0) * 20; if (SelectedAiTarget?.Entity != null) { @@ -31,61 +22,57 @@ namespace Barotrauma //GUI.DrawString(spriteBatch, pos + textOffset, $"AI TARGET: {SelectedAiTarget.Entity.ToString()}", Color.White, Color.Black); } - GUI.DrawString(spriteBatch, pos + textOffset, Character.Name, Color.White, Color.Black); + Vector2 stringDrawPos = pos + textOffset; + GUI.DrawString(spriteBatch, stringDrawPos, Character.Name, Color.White, Color.Black); - if (ObjectiveManager != null) + var currentOrder = ObjectiveManager.CurrentOrder; + if (ObjectiveManager.CurrentOrders.Any()) { - var currentOrder = ObjectiveManager.CurrentOrder; - if (currentOrder != null) + var currentOrders = ObjectiveManager.CurrentOrders; + currentOrders.Sort((x, y) => y.ManualPriority.CompareTo(x.ManualPriority)); + for (int i = 0; i < currentOrders.Count; i++) { - GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(0, 20), $"ORDER: {currentOrder.DebugTag} ({currentOrder.Priority.FormatZeroDecimal()})", Color.White, Color.Black); + stringDrawPos += new Vector2(0, 20); + var order = currentOrders[i]; + GUI.DrawString(spriteBatch, stringDrawPos, $"ORDER {i + 1}: {order.Objective.DebugTag} ({order.Objective.Priority.FormatZeroDecimal()})", Color.White, Color.Black); } - else if (ObjectiveManager.WaitTimer > 0) + } + else if (ObjectiveManager.WaitTimer > 0) + { + stringDrawPos += new Vector2(0, 20); + GUI.DrawString(spriteBatch, stringDrawPos - textOffset, $"Waiting... {ObjectiveManager.WaitTimer.FormatZeroDecimal()}", Color.White, Color.Black); + } + var currentObjective = ObjectiveManager.CurrentObjective; + if (currentObjective != null) + { + int offset = currentOrder != null ? 20 + ((ObjectiveManager.CurrentOrders.Count - 1) * 20) : 0; + if (currentOrder == null || currentOrder.Priority <= 0) { - GUI.DrawString(spriteBatch, pos + new Vector2(0, 20), $"Waiting... {ObjectiveManager.WaitTimer.FormatZeroDecimal()}", Color.White, Color.Black); + stringDrawPos += new Vector2(0, 20); + GUI.DrawString(spriteBatch, stringDrawPos, $"MAIN OBJECTIVE: {currentObjective.DebugTag} ({currentObjective.Priority.FormatZeroDecimal()})", Color.White, Color.Black); } - var currentObjective = ObjectiveManager.CurrentObjective; - if (currentObjective != null) + var subObjective = currentObjective.CurrentSubObjective; + if (subObjective != 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); - } + stringDrawPos += new Vector2(0, 20); + GUI.DrawString(spriteBatch, stringDrawPos, $"SUBOBJECTIVE: {subObjective.DebugTag} ({subObjective.Priority.FormatZeroDecimal()})", Color.White, Color.Black); } - for (int i = 0; i < ObjectiveManager.Objectives.Count; i++) + var activeObjective = ObjectiveManager.GetActiveObjective(); + if (activeObjective != null) { - var objective = ObjectiveManager.Objectives[i]; - int offsetMultiplier; - if (ObjectiveManager.CurrentOrder == null) - { - if (i == 0) - { - continue; - } - else - { - offsetMultiplier = i - 1; - } - } - 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); + stringDrawPos += new Vector2(0, 20); + GUI.DrawString(spriteBatch, stringDrawPos, $"ACTIVE OBJECTIVE: {activeObjective.DebugTag} ({activeObjective.Priority.FormatZeroDecimal()})", Color.White, Color.Black); } } + Vector2 objectiveStringDrawPos = stringDrawPos + new Vector2(120, 40); + for (int i = 0; i < ObjectiveManager.Objectives.Count; i++) + { + var objective = ObjectiveManager.Objectives[i]; + GUI.DrawString(spriteBatch, objectiveStringDrawPos, $"{objective.DebugTag} ({objective.Priority.FormatZeroDecimal()})", Color.White, Color.Black * 0.5f); + objectiveStringDrawPos += new Vector2(0, 18); + } + if (steeringManager is IndoorsSteeringManager pathSteering) { var path = pathSteering.CurrentPath; @@ -111,13 +98,21 @@ namespace Barotrauma new Vector2(path.CurrentNode.DrawPosition.X, -path.CurrentNode.DrawPosition.Y), Color.BlueViolet, 0, 3); - GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(0, 100), "Path cost: " + path.Cost.FormatZeroDecimal(), Color.White, Color.Black * 0.5f); + GUI.DrawString(spriteBatch, stringDrawPos + new Vector2(0, 40), "Path cost: " + path.Cost.FormatZeroDecimal(), Color.White, Color.Black * 0.5f); } } } 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 5dc9cab53..deecf8be6 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; @@ -520,7 +520,7 @@ namespace Barotrauma foreach (Character c in CharacterList) { - if (!CanInteractWith(c, checkVisibility: false)) continue; + if (!CanInteractWith(c, checkVisibility: false) || (c.AnimController?.SimplePhysicsEnabled ?? true)) { continue; } float dist = Vector2.DistanceSquared(mouseSimPos, c.SimPosition); if (dist < maxDist * maxDist && (closestCharacter == null || dist < closestDist)) @@ -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); @@ -634,9 +634,9 @@ namespace Barotrauma } } - partial void SetOrderProjSpecific(Order order, string orderOption) + partial void SetOrderProjSpecific(Order order, string orderOption, int priority) { - GameMain.GameSession?.CrewManager?.AddCurrentOrderIcon(this, order, orderOption); + GameMain.GameSession?.CrewManager?.AddCurrentOrderIcon(this, order, orderOption, priority); } public static void AddAllToGUIUpdateList() @@ -686,7 +686,7 @@ namespace Barotrauma public virtual void DrawFront(SpriteBatch spriteBatch, Camera cam) { - if (!Enabled || InvisibleTimer > 0.0f) { return; } + if (!Enabled || InvisibleTimer > 0.0f || (AnimController?.SimplePhysicsEnabled ?? true)) { return; } if (GameMain.DebugDraw) { @@ -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) { @@ -815,7 +815,7 @@ namespace Barotrauma iconPos.Y = -iconPos.Y; nameColor = iconStyle.Color; var icon = iconStyle.Sprites[GUIComponent.ComponentState.None].First(); - float iconScale = 30.0f / icon.Sprite.size.X / cam.Zoom; + float iconScale = (30.0f / icon.Sprite.size.X / cam.Zoom) * GUI.Scale; icon.Sprite.Draw(spriteBatch, iconPos + new Vector2(-35.0f, -25.0f), iconStyle.Color * hudInfoAlpha, scale: iconScale); } } @@ -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..54f48913e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -1,4 +1,5 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using FarseerPhysics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; @@ -57,7 +58,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 +77,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) { @@ -130,17 +131,6 @@ namespace Barotrauma { character.Inventory.ClearSubInventories(); } - - for (int i = 0; i < character.Inventory.Items.Length - 1; i++) - { - var item = character.Inventory.Items[i]; - if (item == null || character.Inventory.SlotTypes[i] == InvSlotType.Any) continue; - - foreach (ItemComponent ic in item.Components) - { - if (ic.DrawHudWhenEquipped) ic.UpdateHUD(character, deltaTime, cam); - } - } } if (character.IsHumanoid && character.SelectedCharacter != null && character.SelectedCharacter.Inventory != null) @@ -206,22 +196,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 (character.GetCurrentOrderWithTopPriority()?.Order is Order currentOrder && DrawIcon(currentOrder)) { - DrawOrderIndicator(spriteBatch, cam, character, character.CurrentOrder, 1.0f); - } + DrawOrderIndicator(spriteBatch, cam, 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 +230,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 +243,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.None(it => it?.GetComponent() != null))) { if (character.FocusedCharacter != null && character.FocusedCharacter.CanBeSelected) { @@ -281,7 +280,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 +290,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 +306,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 +340,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 +353,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 +430,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 +456,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 +495,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 +518,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 cf337b8e4..2427989ad 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -152,48 +152,37 @@ 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); } } private void GetDisguisedSprites(IdCard idCard) { + if (idCard.Item.Tags == string.Empty) return; + if (idCard.StoredJobPrefab == null || idCard.StoredPortrait == null) { string[] readTags = idCard.Item.Tags.Split(','); + if (readTags.Length == 0) return; + if (idCard.StoredJobPrefab == null) { - string jobIdTag = readTags.First(s => s.StartsWith("jobid:")); + string jobIdTag = readTags.FirstOrDefault(s => s.StartsWith("jobid:")); - if (jobIdTag != string.Empty && jobIdTag.Length > 6) + if (jobIdTag != null && jobIdTag.Length > 6) { string jobId = jobIdTag.Substring(6); if (jobId != string.Empty) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index e02363bf8..67b51da47 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -292,7 +292,7 @@ namespace Barotrauma break; case ServerNetObject.ENTITY_EVENT: - int eventType = msg.ReadRangedInteger(0, 5); + int eventType = msg.ReadRangedInteger(0, 6); switch (eventType) { case 0: //NetEntityEvent.Type.InventoryState @@ -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 @@ -390,6 +390,19 @@ namespace Barotrauma byte campaignInteractionType = msg.ReadByte(); (GameMain.GameSession?.GameMode as CampaignMode)?.AssignNPCMenuInteraction(this, (CampaignMode.InteractionType)campaignInteractionType); break; + case 6: //NetEntityEvent.Type.ObjectiveManagerOrderState + bool properData = msg.ReadBoolean(); + if (!properData) { break; } + int orderIndex = msg.ReadRangedInteger(0, Order.PrefabList.Count); + var orderPrefab = Order.PrefabList[orderIndex]; + string option = null; + if (orderPrefab.HasOptions) + { + int optionIndex = msg.ReadRangedInteger(0, orderPrefab.Options.Length); + option = orderPrefab.Options[optionIndex]; + } + GameMain.GameSession.CrewManager.SetHighlightedOrderIcon(this, orderPrefab.Identifier, option); + break; } msg.ReadPadBits(); break; @@ -434,20 +447,22 @@ 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) { (GameMain.GameSession.GameMode as CampaignMode)?.AssignNPCMenuInteraction(character, character.CampaignInteractionType); } - // Check if the character has a current order - if (inc.ReadBoolean()) + // Check if the character has current orders + int orderCount = inc.ReadByte(); + for (int i = 0; i < orderCount; i++) { int orderPrefabIndex = inc.ReadByte(); Entity targetEntity = FindEntityByID(inc.ReadUInt16()); Character orderGiver = inc.ReadBoolean() ? FindEntityByID(inc.ReadUInt16()) as Character : null; int orderOptionIndex = inc.ReadByte(); + int orderPriority = inc.ReadByte(); OrderTarget targetPosition = null; if (inc.ReadBoolean()) { @@ -468,7 +483,7 @@ namespace Barotrauma new Order(orderPrefab, targetPosition, orderGiver: orderGiver); character.SetOrder(order, orderOptionIndex >= 0 && orderOptionIndex < orderPrefab.Options.Length ? orderPrefab.Options[orderOptionIndex] : null, - orderGiver, speak: false); + orderPriority, orderGiver, speak: false); } else { @@ -487,7 +502,7 @@ namespace Barotrauma character.ReadStatus(inc); } - if (character.IsHuman && character.TeamID != TeamType.FriendlyNPC && !character.IsDead) + if (character.IsHuman && character.TeamID != CharacterTeamType.FriendlyNPC && character.TeamID != CharacterTeamType.None && !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..2a0b43bb6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -668,12 +668,17 @@ namespace Barotrauma bloodParticleTimer -= deltaTime * (affliction.Strength / 10.0f); if (bloodParticleTimer <= 0.0f) { + var emitter = Character.BloodEmitters.FirstOrDefault(); + float particleMinScale = emitter != null ? emitter.Prefab.ScaleMin : 0.5f; + float particleMaxScale = emitter != null ? emitter.Prefab.ScaleMax : 1; + float severity = Math.Min(affliction.Strength / affliction.Prefab.MaxStrength * Character.Params.BleedParticleMultiplier, 1); + float bloodParticleSize = MathHelper.Lerp(particleMinScale, particleMaxScale, severity); bool inWater = Character.AnimController.InWater; - float bloodParticleSize = MathHelper.Lerp(0.5f, 1.0f, affliction.Strength / 100.0f); if (!inWater) { bloodParticleSize *= 2.0f; } + var blood = GameMain.ParticleManager.CreateParticle( inWater ? Character.Params.BleedParticleWater : Character.Params.BleedParticleAir, targetLimb.WorldPosition, Rand.Vector(affliction.Strength), 0.0f, Character.AnimController.CurrentHull); @@ -682,7 +687,7 @@ namespace Barotrauma { blood.Size *= bloodParticleSize; } - bloodParticleTimer = 1.0f; + bloodParticleTimer = MathHelper.Lerp(2, 0.5f, severity); } } @@ -912,7 +917,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 +1637,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 +1657,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 +1801,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 fb33016be..7252476c8 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); + } } } @@ -274,7 +299,12 @@ namespace Barotrauma { var textContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.0f), listBox.Content.RectTransform), style: "InnerFrame", color: Color.White) { - CanBeFocused = false + CanBeFocused = true, + OnSecondaryClicked = (component, data) => + { + GUIContextMenu.CreateContextMenu(new ContextMenuOption("editor.copytoclipboard", true, () => { Clipboard.SetText(msg.Text); })); + return true; + } }; var textBlock = new GUITextBlock(new RectTransform(new Point(listBox.Content.Rect.Width - 5, 0), textContainer.RectTransform, Anchor.TopLeft) { AbsoluteOffset = new Point(2, 2) }, msg.Text, textAlignment: Alignment.TopLeft, font: GUI.SmallFont, wrap: true) @@ -455,7 +485,7 @@ namespace Barotrauma var subInfo = new SubmarineInfo(string.Join(" ", args)); Submarine.MainSub = Submarine.Load(subInfo, true); } - GameMain.SubEditorScreen.Select(); + GameMain.SubEditorScreen.Select(enableAutoSave: Screen.Selected != GameMain.GameScreen); }, isCheat: true)); commands.Add(new Command("editparticles|particleeditor", "editparticles/particleeditor: Switch to the Particle Editor to edit particle effects.", (string[] args) => @@ -487,6 +517,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 +545,102 @@ 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("spreadsheetexport", "Export items in format recognized by the spreadsheet importer.", (string[] args) => + { + SpreadsheetExport.Export(); + })); + + 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 +654,19 @@ 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); + AssignRelayToServer("spreadsheetexport", 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) => { })); @@ -552,14 +704,15 @@ namespace Barotrauma AssignOnExecute("explosion", (string[] args) => { Vector2 explosionPos = GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition); - float range = 500, force = 10, damage = 50, structureDamage = 10, itemDamage = 100, empStrength = 0.0f; + float range = 500, force = 10, damage = 50, structureDamage = 10, itemDamage = 100, empStrength = 0.0f, ballastFloraStrength = 50f; if (args.Length > 0) float.TryParse(args[0], out range); if (args.Length > 1) float.TryParse(args[1], out force); if (args.Length > 2) float.TryParse(args[2], out damage); if (args.Length > 3) float.TryParse(args[3], out structureDamage); if (args.Length > 4) float.TryParse(args[4], out itemDamage); if (args.Length > 5) float.TryParse(args[5], out empStrength); - new Explosion(range, force, damage, structureDamage, itemDamage, empStrength).Explode(explosionPos, null); + if (args.Length > 6) float.TryParse(args[6], out ballastFloraStrength); + new Explosion(range, force, damage, structureDamage, itemDamage, empStrength, ballastFloraStrength).Explode(explosionPos, null); }); AssignOnExecute("teleportcharacter|teleport", (string[] args) => @@ -997,6 +1150,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; @@ -1382,6 +1546,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) => { @@ -1416,6 +1590,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(); @@ -1831,15 +2011,22 @@ namespace Barotrauma ToolBox.OpenFileWithShell(Path.GetFullPath(filePath)); })); #if DEBUG + commands.Add(new Command("playovervc", "Plays a sound over voice chat.", (args) => + { + VoipCapture.Instance?.SetOverrideSound(args.Length > 0 ? args[0] : null); + })); + 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) => @@ -2236,6 +2423,37 @@ namespace Barotrauma } ); +#if DEBUG + commands.Add(new Command("setcurrentlocationtype", "setcurrentlocationtype [location type]: Change the type of the current location.", (string[] args) => + { + var character = Character.Controlled; + if (GameMain.GameSession?.Campaign == null) + { + ThrowError("Campaign not active!"); + return; + } + if (args.Length == 0) + { + ThrowError("Please give the location type after the command."); + return; + } + var locationType = LocationType.List.Find(lt => lt.Identifier.Equals(args[0], StringComparison.OrdinalIgnoreCase)); + if (locationType == null) + { + ThrowError($"Could not find the location type \"{args[0]}\"."); + return; + } + GameMain.GameSession.Campaign.Map.CurrentLocation.ChangeType(locationType); + }, + () => + { + return new string[][] + { + LocationType.List.Select(lt => lt.Identifier).ToArray() + }; + })); +#endif + commands.Add(new Command("limbscale", "Define the limbscale for the controlled character. Provide id or name if you want to target another character. Note: the changes are not saved!", (string[] args) => { var character = Character.Controlled; 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/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs new file mode 100644 index 000000000..de730f0fc --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AbandonedOutpostMission.cs @@ -0,0 +1,30 @@ +using Barotrauma.Networking; + +namespace Barotrauma +{ + partial class AbandonedOutpostMission : Mission + { + public override void ClientReadInitial(IReadMessage msg) + { + byte characterCount = msg.ReadByte(); + + for (int i = 0; i < characterCount; i++) + { + characters.Add(Character.ReadSpawnData(msg)); + ushort itemCount = msg.ReadUInt16(); + for (int j = 0; j < itemCount; j++) + { + Item.ReadSpawnData(msg); + } + } + if (characters.Contains(null)) + { + throw new System.Exception("Error in AbandonedOutpostMission.ClientReadInitial: character list contains null (mission: " + Prefab.Identifier + ")"); + } + if (characters.Count != characterCount) + { + throw new System.Exception("Error in AbandonedOutpostMission.ClientReadInitial: character count does not match the server count (" + characters + " != " + characters.Count + "mission: " + Prefab.Identifier + ")"); + } + } + } +} \ No newline at end of file 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/MissionMode.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionMode.cs index 24e8e3db5..2428d90e2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionMode.cs @@ -1,19 +1,17 @@ -using Microsoft.Xna.Framework; -using System; - -namespace Barotrauma +namespace Barotrauma { abstract partial class MissionMode : GameMode { public override void ShowStartMessage() { - if (mission == null) return; - - new GUIMessageBox(mission.Name, mission.Description, new string[0], type: GUIMessageBox.Type.InGame, icon: mission.Prefab.Icon) + foreach (Mission mission in missions) { - IconColor = mission.Prefab.IconColor, - UserData = "missionstartmessage" - }; + new GUIMessageBox(mission.Name, mission.Description, new string[0], type: GUIMessageBox.Type.InGame, icon: mission.Prefab.Icon) + { + IconColor = mission.Prefab.IconColor, + UserData = "missionstartmessage" + }; + } } } } 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/CrewManagement.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs index b84ba031d..a4d1aae70 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs @@ -91,9 +91,10 @@ namespace Barotrauma UserData = "container" }; + int panelMaxWidth = (int)(GUI.xScale * (GUI.HorizontalAspectRatio < 1.4f ? 650 : 560)); var availableMainGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.4f, 1.0f), campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).RectTransform) { - MaxSize = new Point(560, campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).Rect.Height) + MaxSize = new Point(panelMaxWidth, campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).Rect.Height) }) { Stretch = true, @@ -149,7 +150,7 @@ namespace Barotrauma var pendingAndCrewMainGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.4f, 1.0f), campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).RectTransform, anchor: Anchor.TopRight) { - MaxSize = new Point(560, campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).Rect.Height) + MaxSize = new Point(panelMaxWidth, campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).Rect.Height) }) { Stretch = true, @@ -177,7 +178,7 @@ namespace Barotrauma var pendingAndCrewGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.95f), anchor: Anchor.Center, parent: new GUIFrame(new RectTransform(new Vector2(1.0f, 13.25f / 14.0f), pendingAndCrewMainGroup.RectTransform) { - MaxSize = new Point(560, campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).Rect.Height) + MaxSize = new Point(panelMaxWidth, campaignUI.GetTabContainer(CampaignMode.InteractionType.Crew).Rect.Height) }).RectTransform)); float height = 0.05f; @@ -335,7 +336,7 @@ namespace Barotrauma jobColor = characterInfo.Job.Prefab.UIColor; } - GUIFrame frame = new GUIFrame(new RectTransform(new Point(listBox.Content.Rect.Width, 55), parent: listBox.Content.RectTransform), "ListBoxElement") + GUIFrame frame = new GUIFrame(new RectTransform(new Point(listBox.Content.Rect.Width, (int)(GUI.yScale * 55)), parent: listBox.Content.RectTransform), "ListBoxElement") { UserData = new Tuple(characterInfo, skill != null ? skill.Level : 0.0f) }; @@ -366,7 +367,7 @@ namespace Barotrauma jobBlock.Text = ToolBox.LimitString(jobBlock.Text, jobBlock.Font, jobBlock.Rect.Width); float width = 0.6f / 3; - if (characterInfo.Job != null) + if (characterInfo.Job != null && skill != null) { GUILayoutGroup skillGroup = new GUILayoutGroup(new RectTransform(new Vector2(width, 0.6f), mainGroup.RectTransform), isHorizontal: true); float iconWidth = (float)skillGroup.Rect.Height / skillGroup.Rect.Width; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index 3c92b6bf4..a2321dd30 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) { @@ -854,6 +855,7 @@ namespace Barotrauma lock (mutex) { GUIMessageBox.AddActiveToGUIUpdateList(); + GUIContextMenu.AddActiveToGUIUpdateList(); if (pauseMenuOpen) { @@ -920,6 +922,7 @@ namespace Barotrauma if ((!PlayerInput.PrimaryMouseButtonHeld() && !PlayerInput.PrimaryMouseButtonClicked()) || c == prevMouseOn) { MouseOn = c; + var sakdjfnsjkd = c.MouseRect; } break; } @@ -1117,15 +1120,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 +1233,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 +1258,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 +1270,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; + float symbolScale = Math.Min(64.0f / sprite.size.X, 1.0f) * scaleMultiplier * Scale; - 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 +1296,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 +1528,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 +1546,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 +1647,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 +2119,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 +2328,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/GUIContextMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs new file mode 100644 index 000000000..f752af7c2 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIContextMenu.cs @@ -0,0 +1,292 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + struct ContextMenuOption + { + public string Label; + public Action OnSelected; + public ContextMenuOption[]? SubOptions; + public bool IsEnabled; + public string Tooltip; + + // Creates a regular context menu + public ContextMenuOption(string label, bool isEnabled, Action onSelected) + { + Label = TextManager.Get(label, returnNull: true) ?? label; + OnSelected = onSelected; + IsEnabled = isEnabled; + SubOptions = null; + Tooltip = string.Empty; + } + + // Creates a option with a sub context menu + public ContextMenuOption(string label, bool isEnabled, params ContextMenuOption[] options): this(label, isEnabled, () => { }) + { + SubOptions = options; + } + } + + internal class GUIContextMenu : GUIComponent + { + public static GUIContextMenu? CurrentContextMenu; + + private readonly Dictionary Options = new Dictionary(); + private GUIContextMenu? SubMenu; + public readonly GUITextBlock? HeaderLabel; + public GUITextBlock? ParentOption; + + /// + /// Creates a context menu. This constructor does not make the context menu active. + /// Use to make right click context menus. + /// + /// Position at which to create the context menu + /// Header text + /// Background style + /// list of context menu options + public GUIContextMenu(Vector2? position, string header, string style, params ContextMenuOption[] options) : base(style, new RectTransform(Point.Zero, GUI.Canvas)) + { + Vector2 pos = position ?? PlayerInput.MousePosition; + ScalableFont headerFont = GUI.SubHeadingFont; + ScalableFont font = GUI.SmallFont; // font the context menu options use + Vector4 padding = new Vector4(4), headerPadding = new Vector4(8); + int horizontalPadding = (int) (padding.X + padding.Z), verticalPadding = (int) (padding.Y + padding.W); + bool hasHeader = !string.IsNullOrWhiteSpace(header); + + //---------------------------------------------------------------------------------- + // Estimate the size of the context menu + //---------------------------------------------------------------------------------- + + Dictionary optionsAndSizes = new Dictionary(); + + // estimate how big the context menu needs to be + Point estimatedSize = new Point(horizontalPadding, verticalPadding); + + if (hasHeader) + { + InflateSize(ref estimatedSize, header, headerFont); + } + + foreach (ContextMenuOption option in options) + { + Vector2 optionSize = InflateSize(ref estimatedSize, option.Label, font); + optionsAndSizes.Add(option, optionSize); + } + + // it's better to overestimate the size since it's going to be cropped anyways + estimatedSize = estimatedSize.Multiply(1.2f); + + RectTransform.NonScaledSize = estimatedSize; + RectTransform.AbsoluteOffset = pos.ToPoint(); + + //---------------------------------------------------------------------------------- + // Construct the GUI elements + //---------------------------------------------------------------------------------- + + GUILayoutGroup background = new GUILayoutGroup(new RectTransform(Vector2.One, RectTransform, Anchor.Center)); + + if (hasHeader) + { + HeaderLabel = new GUITextBlock(new RectTransform(new Vector2(1f, 0.2f), background.RectTransform), header, font: headerFont) { Padding = headerPadding }; + } + + GUIListBox optionList = new GUIListBox(new RectTransform(new Vector2(1f, hasHeader ? 0.8f : 1f), background.RectTransform), style: null) + { + AutoHideScrollBar = false, + ScrollBarVisible = false, + Padding = hasHeader ? new Vector4(4, 0, 4, 4) : padding + }; + + foreach (var (option, size) in optionsAndSizes) + { + GUITextBlock optionElement = new GUITextBlock(new RectTransform(size.ToPoint(), optionList.Content.RectTransform), option.Label, font: font) + { + UserData = option, + Enabled = option.IsEnabled + }; + Options.Add(option, optionElement); + + if (!string.IsNullOrWhiteSpace(option.Tooltip) && optionElement.Enabled) + { + optionElement.ToolTip = option.Tooltip; + } + + if (!option.IsEnabled) + { + optionElement.TextColor *= 0.5f; + } + } + + //---------------------------------------------------------------------------------- + // Positioning and cropping the context menu + //---------------------------------------------------------------------------------- + + List children = optionList.Content.Children.ToList(); + + // Resize all children to the size of their text + foreach (GUITextBlock block in children.Where(c => c is GUITextBlock).Cast()) + { + block.RectTransform.NonScaledSize = new Point((int) (block.TextSize.X + (block.Padding.X + block.Padding.Z)), (int) (18 * GUI.Scale)); + } + + int largestWidth = children.Max(c => c.Rect.Width + horizontalPadding); + + // if the header is bigger than any of the options then overwrite + if (HeaderLabel != null) + { + RectTransform headerTransform = HeaderLabel.RectTransform; + headerTransform.MinSize = new Point((int) (HeaderLabel.TextSize.X + (headerPadding.X + headerPadding.Z)), headerTransform.NonScaledSize.Y); + if (largestWidth < headerTransform.MinSize.X) + { + largestWidth = headerTransform.MinSize.X; + } + } + + // resize all children to the size of the longest element + foreach (GUIComponent c in children) + { + c.RectTransform.MinSize = new Point(largestWidth, c.Rect.Height); + } + + // the cropped size of the option list + Point newSize = new Point(largestWidth, children.Sum(c => c.Rect.Height) + verticalPadding); + // resize the menu itself taking into account the option menus relative Y size + RectTransform.NonScaledSize = new Point(newSize.X, (int) (newSize.Y / optionList.RectTransform.RelativeSize.Y)); + optionList.RectTransform.NonScaledSize = newSize; + + // move the context menu if it would go outside of screen + if (RectTransform.Rect.Bottom > GameMain.GraphicsHeight) + { + Rectangle rect = RectTransform.Rect; + RectTransform.AbsoluteOffset = new Point(rect.X, rect.Y - rect.Height); + } + + if (RectTransform.Rect.Right > GameMain.GraphicsWidth) + { + Rectangle rect = RectTransform.Rect; + RectTransform.AbsoluteOffset = new Point(rect.X - rect.Width, rect.Y); + } + + background.Recalculate(); + + optionList.OnSelected = OnSelected; + } + + public static GUIContextMenu CreateContextMenu(params ContextMenuOption[] options) => CreateContextMenu(PlayerInput.MousePosition, string.Empty, null, options); + + public static GUIContextMenu CreateContextMenu(Vector2? pos, string header, Color? headerColor, params ContextMenuOption[] options) + { + GUIContextMenu menu = new GUIContextMenu(pos,header, "GUIToolTip", options); + if (headerColor != null) + { + menu.HeaderLabel?.OverrideTextColor(headerColor.Value); + } + CurrentContextMenu = menu; + return menu; + } + + private bool OnSelected(GUIComponent _, object data) + { + if (data is ContextMenuOption option && option.IsEnabled) + { + CurrentContextMenu = null; + option.OnSelected(); + return true; + } + + return false; + } + + /// + /// Inflates a point by the size of the text + /// + /// Pint to resize + /// String whose size to inflate by + /// What font to use + /// The size of the text + private Vector2 InflateSize(ref Point size, string label, ScalableFont font) + { + Vector2 textSize = font.MeasureString(label); + size.X = Math.Max((int) Math.Ceiling(textSize.X), size.X); + size.Y += (int) Math.Ceiling(textSize.Y); + return textSize; + } + + protected override void Update(float deltaTime) + { + base.Update(deltaTime); + + // keep the parent highlighted + if (ParentOption != null) + { + ParentOption.State = ComponentState.Hover; + } + + if (SubMenu != null && !SubMenu.IsMouseOver()) + { + SubMenu = null; + return; + } + + foreach (var (option, textBlock) in Options) + { + // Create a new sub context menu if hovering over an option with sub options + if (GUI.MouseOn != textBlock) { continue; } + if (option.IsEnabled && option.SubOptions is { } subOptions && subOptions.Any()) + { + Vector2 subMenuPos = new Vector2(textBlock.MouseRect.Right + 4, textBlock.MouseRect.Y); + SubMenu = new GUIContextMenu(subMenuPos, "", "GUIToolTip", subOptions) + { + ParentOption = textBlock + }; + } + } + } + + /// + /// Checks if the mouse cursor is over this context menu or any of its sub menus + /// + /// + private bool IsMouseOver() + { + Rectangle expandedRect = Rect; + expandedRect.Inflate(20, 20); + + bool isMouseOn = expandedRect.Contains(PlayerInput.MousePosition); + + if (ParentOption != null) + { + isMouseOn |= GUI.MouseOn == ParentOption; + } + + // Recursively check sub context menus + if (!isMouseOn && SubMenu != null) + { + isMouseOn = SubMenu.IsMouseOver(); + } + + return isMouseOn; + } + + public override void AddToGUIUpdateList(bool ignoreChildren = false, int order = 0) + { + base.AddToGUIUpdateList(ignoreChildren, order); + SubMenu?.AddToGUIUpdateList(); + } + + public static void AddActiveToGUIUpdateList() + { + if (CurrentContextMenu != null && !CurrentContextMenu.IsMouseOver()) + { + CurrentContextMenu = null; + } + + CurrentContextMenu?.AddToGUIUpdateList(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs index 5f1c22356..7d5233b38 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs @@ -214,7 +214,22 @@ namespace Barotrauma public bool AutoHideScrollBar { get; set; } = true; private bool IsScrollBarOnDefaultSide { get; set; } - public bool CanDragElements { get; set; } = false; + public bool CanDragElements + { + get + { + return canDragElements; + } + set + { + if (value == false && canDragElements && draggedElement != null) + { + draggedElement = null; + } + canDragElements = value; + } + } + private bool canDragElements = false; private GUIComponent draggedElement; private Rectangle draggedReferenceRectangle; private Point draggedReferenceOffset; @@ -223,9 +238,12 @@ namespace Barotrauma private bool scheduledScroll = false; + private readonly bool isHorizontal; + /// For horizontal listbox, default side is on the bottom. For vertical, it's on the right. public GUIListBox(RectTransform rectT, bool isHorizontal = false, Color? color = null, string style = "", bool isScrollBarOnDefaultSide = true, bool useMouseDownToSelect = false) : base(style, rectT) { + this.isHorizontal = isHorizontal; HoverCursor = CursorState.Hand; CanBeFocused = true; selected = new List(); @@ -454,22 +472,42 @@ namespace Barotrauma } else { - draggedElement.RectTransform.AbsoluteOffset = draggedReferenceOffset + new Point(0, (int)PlayerInput.MousePosition.Y - draggedReferenceRectangle.Center.Y); + draggedElement.RectTransform.AbsoluteOffset = isHorizontal ? + draggedReferenceOffset + new Point((int)PlayerInput.MousePosition.X - draggedReferenceRectangle.Center.X, 0) : + draggedReferenceOffset + new Point(0, (int)PlayerInput.MousePosition.Y - draggedReferenceRectangle.Center.Y); int index = Content.RectTransform.GetChildIndex(draggedElement.RectTransform); int currIndex = index; - while (currIndex > 0 && PlayerInput.MousePosition.Y < draggedReferenceRectangle.Top) + if (isHorizontal) { - currIndex--; - draggedReferenceRectangle.Y -= draggedReferenceRectangle.Height; - draggedReferenceOffset.Y -= draggedReferenceRectangle.Height; + while (currIndex > 0 && PlayerInput.MousePosition.X < draggedReferenceRectangle.Left) + { + currIndex--; + draggedReferenceRectangle.X -= draggedReferenceRectangle.Width; + draggedReferenceOffset.X -= draggedReferenceRectangle.Width; + } + while (currIndex < Content.CountChildren - 1 && PlayerInput.MousePosition.X > draggedReferenceRectangle.Right) + { + currIndex++; + draggedReferenceRectangle.X += draggedReferenceRectangle.Width; + draggedReferenceOffset.X += draggedReferenceRectangle.Width; + } } - while (currIndex < Content.CountChildren - 1 && PlayerInput.MousePosition.Y > draggedReferenceRectangle.Bottom) + else { - currIndex++; - draggedReferenceRectangle.Y += draggedReferenceRectangle.Height; - draggedReferenceOffset.Y += draggedReferenceRectangle.Height; + while (currIndex > 0 && PlayerInput.MousePosition.Y < draggedReferenceRectangle.Top) + { + currIndex--; + draggedReferenceRectangle.Y -= draggedReferenceRectangle.Height; + draggedReferenceOffset.Y -= draggedReferenceRectangle.Height; + } + while (currIndex < Content.CountChildren - 1 && PlayerInput.MousePosition.Y > draggedReferenceRectangle.Bottom) + { + currIndex++; + draggedReferenceRectangle.Y += draggedReferenceRectangle.Height; + draggedReferenceOffset.Y += draggedReferenceRectangle.Height; + } } if (currIndex != index) @@ -510,10 +548,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; @@ -943,9 +981,10 @@ namespace Barotrauma public override void RemoveChild(GUIComponent child) { - if (child == null) { return; } + if (child == null) { return; } child.RectTransform.Parent = null; - if (selected.Contains(child)) selected.Remove(child); + if (selected.Contains(child)) { selected.Remove(child); } + if (draggedElement == child) { draggedElement = null; } UpdateScrollBarSize(); } 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/GUINumberInput.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs index a2e32d741..dae7f109d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUINumberInput.cs @@ -81,10 +81,13 @@ namespace Barotrauma private float floatValue; public float FloatValue { - get { return floatValue; } + get + { + return floatValue; + } set { - if (MathUtils.NearlyEqual(value, floatValue)) return; + if (MathUtils.NearlyEqual(value, floatValue)) { return; } floatValue = value; ClampFloatValue(); float newValue = floatValue; @@ -129,10 +132,13 @@ namespace Barotrauma private int intValue; public int IntValue { - get { return intValue; } + get + { + return intValue; + } set { - if (value == intValue) return; + if (value == intValue) { return; } intValue = value; UpdateText(); } @@ -192,6 +198,29 @@ namespace Barotrauma }; TextBox.CaretColor = TextBox.TextColor; TextBox.OnTextChanged += TextChanged; + TextBox.OnDeselected += (sender, key) => + { + if (inputType == NumberType.Int) + { + ClampIntValue(); + } + else + { + ClampFloatValue(); + } + }; + TextBox.OnEnterPressed += (textBox, text) => + { + if (inputType == NumberType.Int) + { + ClampIntValue(); + } + else + { + ClampFloatValue(); + } + return true; + }; var buttonArea = new GUIFrame(new RectTransform(new Vector2(_relativeButtonAreaWidth, 1.0f), LayoutGroup.RectTransform, Anchor.CenterRight), style: null); PlusButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.5f), buttonArea.RectTransform), style: null); @@ -299,10 +328,12 @@ namespace Barotrauma if (inputType == NumberType.Int) { IntValue -= valueStep > 0 ? (int)valueStep : 1; + ClampIntValue(); } else if (maxValueFloat.HasValue && minValueFloat.HasValue) { FloatValue -= valueStep > 0 ? valueStep : Round(); + ClampFloatValue(); } } @@ -311,10 +342,12 @@ namespace Barotrauma if (inputType == NumberType.Int) { IntValue += valueStep > 0 ? (int)valueStep : 1; + ClampIntValue(); } else if (inputType == NumberType.Float) { FloatValue += valueStep > 0 ? valueStep : Round(); + ClampFloatValue(); } } @@ -325,7 +358,7 @@ namespace Barotrauma /// private float Round() { - if (!maxValueFloat.HasValue || !minValueFloat.HasValue) return 0; + if (!maxValueFloat.HasValue || !minValueFloat.HasValue) { return 0; } float onePercent = MathHelper.Lerp(minValueFloat.Value, maxValueFloat.Value, 0.01f); float diff = maxValueFloat.Value - minValueFloat.Value; int decimals = (int)MathHelper.Lerp(3, 0, MathUtils.InverseLerp(10, 1000, diff)); @@ -337,28 +370,24 @@ namespace Barotrauma switch (InputType) { case NumberType.Int: - int newIntValue = IntValue; if (string.IsNullOrWhiteSpace(text) || text == "-") { intValue = 0; } - else if (int.TryParse(text, out newIntValue)) + else if (int.TryParse(text, out int newIntValue)) { intValue = newIntValue; } - ClampIntValue(); break; case NumberType.Float: - float newFloatValue = FloatValue; if (string.IsNullOrWhiteSpace(text) || text == "-") { floatValue = 0; } - else if (float.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out newFloatValue)) + else if (float.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out float newFloatValue)) { floatValue = newFloatValue; } - ClampFloatValue(); break; } OnValueChanged?.Invoke(this); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs index c05e96d87..a0a59d81b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs @@ -34,6 +34,8 @@ namespace Barotrauma public readonly Sprite[] CursorSprite = new Sprite[7]; + public UISprite RadiationSprite { get; private set; } + public UISprite UIGlow { get; private set; } public UISprite UIGlowCircular { get; private set; } @@ -70,6 +72,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 +153,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; @@ -205,6 +211,9 @@ namespace Barotrauma case "uiglow": UIGlow = new UISprite(subElement); break; + case "radiation": + RadiationSprite = new UISprite(subElement); + break; case "uiglowcircular": UIGlowCircular = new UISprite(subElement); break; @@ -344,7 +353,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 +363,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..81b438eaa 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,72 @@ 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); + SetPriceGetters(itemFrame, true); } + 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 +700,73 @@ 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); + SetPriceGetters(itemFrame, false); + } + SetItemFrameStatus(itemFrame, hasPermissions && itemQuantity > 0); + if (itemQuantity < 1 && !isRequestedGood) + { + itemFrame.Visible = false; } - if (quantity < 1) { 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); @@ -604,6 +774,37 @@ namespace Barotrauma shoppingCrateSellList.BarScroll = prevShoppingCrateScroll; } + private void SetPriceGetters(GUIComponent itemFrame, bool buying) + { + if (itemFrame == null || !(itemFrame.UserData is PurchasedItem pi)) { return; } + + if (itemFrame.FindChild("undiscountedprice", recursive: true) is GUITextBlock undiscountedPriceBlock) + { + if (buying) + { + undiscountedPriceBlock.TextGetter = () => GetCurrencyFormatted( + CurrentLocation?.GetAdjustedItemBuyPrice(pi.ItemPrefab, considerDailySpecials: false) ?? 0); + } + else + { + undiscountedPriceBlock.TextGetter = () => GetCurrencyFormatted( + CurrentLocation?.GetAdjustedItemSellPrice(pi.ItemPrefab, considerRequestedGoods: false) ?? 0); + } + } + + if (itemFrame.FindChild("price", recursive: true) is GUITextBlock priceBlock) + { + if (buying) + { + priceBlock.TextGetter = () => GetCurrencyFormatted(CurrentLocation?.GetAdjustedItemBuyPrice(pi.ItemPrefab) ?? 0); + } + else + { + priceBlock.TextGetter = () => GetCurrencyFormatted(CurrentLocation?.GetAdjustedItemSellPrice(pi.ItemPrefab) ?? 0); + } + } + } + public void RefreshItemsToSell() { itemsToSell.Clear(); @@ -673,13 +874,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 +909,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 +1054,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 +1062,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 +1106,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 +1168,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 +1199,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 +1208,36 @@ 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 (locationHasDealOnItem) { - priceBlock.TextGetter = () => GetCurrencyFormatted(CurrentLocation?.GetAdjustedItemSellPrice(priceInfo) ?? 0); - } - else - { - priceBlock.TextGetter = () => GetCurrencyFormatted(CurrentLocation?.GetAdjustedItemBuyPrice(priceInfo) ?? 0); + 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" + }; } + SetPriceGetters(frame, !isSellingRelatedList); - if (listBox == storeDealsList || listBox == storeBuyList || listBox == storeSellList) + if (isParentOnLeftSideOfInterface) { new GUIButton(new RectTransform(new Vector2(buttonRelativeWidth, 0.9f), mainGroup.RectTransform), style: "StoreAddToCrateButton") { @@ -902,7 +1260,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 +1288,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 +1347,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 +1479,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 +1513,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 +1575,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..a9b5fc466 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) }) { @@ -858,54 +857,60 @@ namespace Barotrauma int locationInfoYOffset = locationNameText.Rect.Height + locationTypeText.Rect.Height + padding * 2; - GUIFrame missionDescriptionHolder; + GUIListBox missionList; if (hasPortrait) { - GUIFrame missionImageHolder = new GUIFrame(new RectTransform(new Point(contentWidth, (int)(missionFrame.Rect.Height * 0.588f)), missionFrame.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, locationInfoYOffset) }); + GUIFrame portraitHolder = new GUIFrame(new RectTransform(new Point(contentWidth, (int)(missionFrame.Rect.Height * 0.588f)), missionFrame.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, locationInfoYOffset) }); float portraitAspectRatio = portrait.SourceRect.Width / portrait.SourceRect.Height; - GUIImage portraitImage = new GUIImage(new RectTransform(new Vector2(1.0f, 1f), missionImageHolder.RectTransform), portrait, scaleToFit: true); - missionImageHolder.RectTransform.NonScaledSize = new Point(portraitImage.Rect.Size.X, (int)(portraitImage.Rect.Size.X / portraitAspectRatio)); - missionDescriptionHolder = new GUIFrame(new RectTransform(new Point(contentWidth, 0), missionFrame.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, missionImageHolder.RectTransform.AbsoluteOffset.Y + missionImageHolder.Rect.Height + padding) }, style: null); + GUIImage portraitImage = new GUIImage(new RectTransform(new Vector2(1.0f, 1f), portraitHolder.RectTransform), portrait, scaleToFit: true); + portraitHolder.RectTransform.NonScaledSize = new Point(portraitImage.Rect.Size.X, (int)(portraitImage.Rect.Size.X / portraitAspectRatio)); + + missionList = new GUIListBox(new RectTransform(new Point(contentWidth, missionFrame.Rect.Bottom - portraitHolder.Rect.Bottom - padding), missionFrame.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, portraitHolder.RectTransform.AbsoluteOffset.Y + portraitHolder.Rect.Height + padding) }); } else { - missionDescriptionHolder = new GUIFrame(new RectTransform(new Point(contentWidth, 0), missionFrame.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, locationInfoYOffset) }, style: null); - } + missionList = new GUIListBox(new RectTransform(new Point(contentWidth, missionFrame.Rect.Height - locationInfoYOffset - padding), missionFrame.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, locationInfoYOffset) }); + } + missionList.ContentBackground.Color = Color.Transparent; + missionList.Spacing = GUI.IntScale(15); - Mission mission = GameMain.GameSession?.Mission; - if (mission != null) + if (GameMain.GameSession?.Missions != null) { - GUILayoutGroup missionTextGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.744f, 0f), missionDescriptionHolder.RectTransform, Anchor.CenterLeft) { RelativeOffset = new Vector2(0.225f, 0f) }, false, childAnchor: Anchor.TopLeft); - - string missionNameString = ToolBox.WrapText(mission.Name, missionTextGroup.Rect.Width, GUI.LargeFont); - string missionDescriptionString = ToolBox.WrapText(mission.Description, missionTextGroup.Rect.Width, GUI.Font); - string rewardText = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", mission.Reward)); - string missionRewardString = ToolBox.WrapText(TextManager.GetWithVariable("MissionReward", "[reward]", rewardText), missionTextGroup.Rect.Width, GUI.Font); - - Vector2 missionNameSize = GUI.LargeFont.MeasureString(missionNameString); - Vector2 missionDescriptionSize = GUI.Font.MeasureString(missionDescriptionString); - Vector2 missionRewardSize = GUI.Font.MeasureString(missionRewardString); - - missionDescriptionHolder.RectTransform.NonScaledSize = new Point(missionDescriptionHolder.RectTransform.NonScaledSize.X, (int)(missionNameSize.Y + missionDescriptionSize.Y + missionRewardSize.Y)); - missionTextGroup.RectTransform.NonScaledSize = new Point(missionTextGroup.RectTransform.NonScaledSize.X, missionDescriptionHolder.RectTransform.NonScaledSize.Y); - - if (mission.Prefab.Icon != null) + foreach (Mission mission in GameMain.GameSession.Missions) { - float iconAspectRatio = mission.Prefab.Icon.SourceRect.Width / mission.Prefab.Icon.SourceRect.Height; - int iconWidth = (int)(0.225f * missionDescriptionHolder.RectTransform.NonScaledSize.X); - int iconHeight = Math.Max(missionTextGroup.RectTransform.NonScaledSize.Y, (int)(iconWidth * iconAspectRatio)); - Point iconSize = new Point(iconWidth, iconHeight); + GUIFrame missionDescriptionHolder = new GUIFrame(new RectTransform(Vector2.One, missionList.Content.RectTransform), style: null); + GUILayoutGroup missionTextGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.744f, 0f), missionDescriptionHolder.RectTransform, Anchor.CenterLeft) { RelativeOffset = new Vector2(0.225f, 0f) }, false, childAnchor: Anchor.TopLeft); - new GUIImage(new RectTransform(iconSize, missionDescriptionHolder.RectTransform), mission.Prefab.Icon, null, true) { Color = mission.Prefab.IconColor }; - } - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionNameString, font: GUI.LargeFont); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionRewardString); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionDescriptionString); + string missionNameString = ToolBox.WrapText(mission.Name, missionTextGroup.Rect.Width, GUI.LargeFont); + string missionDescriptionString = ToolBox.WrapText(mission.Description, missionTextGroup.Rect.Width, GUI.Font); + string rewardText = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", mission.Reward)); + string missionRewardString = ToolBox.WrapText(TextManager.GetWithVariable("MissionReward", "[reward]", rewardText), missionTextGroup.Rect.Width, GUI.Font); + + Vector2 missionNameSize = GUI.LargeFont.MeasureString(missionNameString); + Vector2 missionDescriptionSize = GUI.Font.MeasureString(missionDescriptionString); + Vector2 missionRewardSize = GUI.Font.MeasureString(missionRewardString); + + missionDescriptionHolder.RectTransform.NonScaledSize = new Point(missionDescriptionHolder.RectTransform.NonScaledSize.X, (int)(missionNameSize.Y + missionDescriptionSize.Y + missionRewardSize.Y)); + missionTextGroup.RectTransform.NonScaledSize = new Point(missionTextGroup.RectTransform.NonScaledSize.X, missionDescriptionHolder.RectTransform.NonScaledSize.Y); + + if (mission.Prefab.Icon != null) + { + float iconAspectRatio = mission.Prefab.Icon.SourceRect.Width / mission.Prefab.Icon.SourceRect.Height; + int iconWidth = (int)(0.225f * missionDescriptionHolder.RectTransform.NonScaledSize.X); + int iconHeight = Math.Max(missionTextGroup.RectTransform.NonScaledSize.Y, (int)(iconWidth * iconAspectRatio)); + Point iconSize = new Point(iconWidth, iconHeight); + + new GUIImage(new RectTransform(iconSize, missionDescriptionHolder.RectTransform), mission.Prefab.Icon, null, true) { Color = mission.Prefab.IconColor, HoverColor = mission.Prefab.IconColor }; + } + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionNameString, font: GUI.LargeFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionRewardString); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionDescriptionString); + } } else { - GUILayoutGroup missionTextGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0f), missionDescriptionHolder.RectTransform, Anchor.CenterLeft), false, childAnchor: Anchor.TopLeft); + GUILayoutGroup missionTextGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0f), missionList.RectTransform, Anchor.CenterLeft), false, childAnchor: Anchor.TopLeft); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), TextManager.Get("NoMission"), font: GUI.LargeFont); } } 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/GUI/Widget.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Widget.cs index f10f5cda3..4627be9c8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Widget.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Widget.cs @@ -32,6 +32,7 @@ namespace Barotrauma public Vector2 DrawPos { get; set; } public int size = 10; + public float thickness = 1f; /// /// Used only for circles. /// @@ -157,7 +158,7 @@ namespace Barotrauma { GUI.DrawRectangle(spriteBatch, drawRect, secondaryColor.Value, isFilled, thickness: 2); } - GUI.DrawRectangle(spriteBatch, drawRect, color, isFilled, thickness: IsSelected ? 3 : 1); + GUI.DrawRectangle(spriteBatch, drawRect, color, isFilled, thickness: IsSelected ? (int)(thickness * 3) : (int)thickness); break; case Shape.Circle: if (secondaryColor.HasValue) @@ -182,7 +183,7 @@ namespace Barotrauma { if (showTooltip && !string.IsNullOrEmpty(tooltip)) { - var offset = tooltipOffset ?? new Vector2(size, -size / 2); + var offset = tooltipOffset ?? new Vector2(size, -size / 2f); GUI.DrawString(spriteBatch, DrawPos + offset, tooltip, textColor, textBackgroundColor); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index 5efb2a598..90432b668 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; @@ -634,6 +635,7 @@ namespace Barotrauma /// protected override void UnloadContent() { + TextureLoader.CancelAll(); CoroutineManager.StopCoroutines("Load"); Video.Close(); VoipCapture.Instance?.Dispose(); @@ -682,7 +684,7 @@ namespace Barotrauma } public void OnLobbyJoinRequested(Steamworks.Data.Lobby lobby, Steamworks.SteamId friendId) - { + { SteamManager.JoinLobby(lobby.Id, true); } @@ -902,7 +904,9 @@ namespace Barotrauma } #if !DEBUG - if (NetworkMember == null && !WindowActive && !Paused && true && Screen.Selected != MainMenuScreen && Config.PauseOnFocusLost) + if (NetworkMember == null && !WindowActive && !Paused && true && Config.PauseOnFocusLost && + Screen.Selected != MainMenuScreen && Screen.Selected != ServerListScreen && Screen.Selected != NetLobbyScreen && + Screen.Selected != SubEditorScreen && Screen.Selected != LevelEditorScreen) { GUI.TogglePauseMenu(); Paused = true; @@ -1072,13 +1076,6 @@ namespace Barotrauma { ((TutorialMode)GameSession.GameMode).Tutorial?.Stop(); } - - if (GameSettings.SendUserStatistics) - { - Mission mission = GameSession.Mission; - GameAnalyticsManager.AddDesignEvent("QuitRound:" + (save ? "Save" : "NoSave")); - GameAnalyticsManager.AddDesignEvent("EndRound:" + (mission == null ? "NoMission" : (mission.Completed ? "MissionCompleted" : "MissionFailed"))); - } } GUIMessageBox.CloseAll(); MainMenuScreen.Select(); @@ -1112,7 +1109,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 5ad4e99e6..a7ca8fedd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -30,9 +30,6 @@ namespace Barotrauma private bool _isCrewMenuOpen = true; private Point crewListEntrySize; - private GUIFrame contextMenu; - private GUIListBox subContextMenu; - /// /// Present only in single player games. In multiplayer. The chatbox is found from GameSession.Client. /// @@ -68,8 +65,6 @@ namespace Barotrauma private Sprite jobIndicatorBackground, previousOrderArrow, cancelIcon; - private const int MaxOrderIcons = 3; - #endregion #region Constructors @@ -134,6 +129,7 @@ namespace Barotrauma isScrollBarOnDefaultSide: false) { AutoHideScrollBar = false, + CanBeFocused = false, OnSelected = (component, userData) => false, SelectMultiple = false, Spacing = (int)(GUI.Scale * 10) @@ -199,7 +195,7 @@ namespace Barotrauma var headset = GetHeadset(Character.Controlled, true); if (headset != null && headset.CanTransmit()) { - headset.TransmitSignal(stepsTaken: 0, signal: msg, source: headset.Item, sender: Character.Controlled, sendToChat: false); + headset.TransmitSignal(stepsTaken: 0, signal: msg, source: headset.Item, sender: Character.Controlled, sentFromChat: true); } } textbox.Deselect(); @@ -222,7 +218,10 @@ namespace Barotrauma var chatBox = ChatBox ?? GameMain.Client?.ChatBox; if (chatBox != null) { - chatBox.ToggleButton = new GUIButton(new RectTransform(new Point((int)(182f * GUI.Scale * 0.4f), (int)(99f * GUI.Scale * 0.4f)), chatBox.GUIFrame.Parent.RectTransform), style: "ChatToggleButton"); + chatBox.ToggleButton = new GUIButton(new RectTransform(new Point((int)(182f * GUI.Scale * 0.4f), (int)(99f * GUI.Scale * 0.4f)), chatBox.GUIFrame.Parent.RectTransform), style: "ChatToggleButton") + { + ClampMouseRectToParent = false + }; chatBox.ToggleButton.RectTransform.AbsoluteOffset = new Point(0, HUDLayoutSettings.ChatBoxArea.Height - chatBox.ToggleButton.Rect.Height); chatBox.ToggleButton.OnClicked += (GUIButton btn, object userdata) => { @@ -232,7 +231,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 +251,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) => @@ -260,12 +259,13 @@ namespace Barotrauma if (!CanIssueOrders) { return false; } var sub = Character.Controlled.Submarine; if (sub == null || sub.TeamID != Character.Controlled.TeamID || sub.Info.IsWreck) { return false; } - SetCharacterOrder(null, order, null, Character.Controlled); + SetCharacterOrder(null, order, null, CharacterInfo.HighestManualOrderPriority, Character.Controlled); if (IsSinglePlayer) { HumanAIController.ReportProblem(Character.Controlled, order); } return true; }, UserData = order, - ToolTip = order.Name + ToolTip = order.Name, + ClampMouseRectToParent = false }; new GUIFrame(new RectTransform(new Vector2(1.5f), btn.RectTransform, Anchor.Center), "OuterGlowCircular") @@ -412,13 +412,13 @@ namespace Barotrauma layoutGroup.RectTransform) { MaxSize = new Point(150, background.Rect.Height) - }, - ToolBox.LimitString(character.Name, font, (int)(nameRelativeWidth * layoutGroup.Rect.Width)), + }, "", font: font, textColor: character.Info?.Job?.Prefab?.UIColor) { CanBeFocused = false }; + nameBlock.Text = ToolBox.LimitString(character.Name, font, (int)nameBlock.Rect.Width); var nameActualRealtiveWidth = Math.Min(nameRelativeWidth * background.Rect.Width, 150) / background.Rect.Width; var characterButton = new GUIButton( @@ -463,6 +463,33 @@ namespace Barotrauma CanBeFocused = false }; + var orderGroup = new GUILayoutGroup(new RectTransform(new Vector2(3 * 0.8f * iconRelativeWidth, 0.8f), parent: layoutGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + CanBeFocused = false, + Stretch = true + }; + + // Current orders + var currentOrderList = new GUIListBox(new RectTransform(new Vector2(0.0f, 1.0f), parent: orderGroup.RectTransform), isHorizontal: true, style: null) + { + AllowMouseWheelScroll = false, + CanDragElements = true, + HideChildrenOutsideFrame = false, + KeepSpaceForScrollBar = false, + OnRearranged = OnOrdersRearranged, + ScrollBarVisible = false, + Spacing = 2, + UserData = character + }; + currentOrderList.RectTransform.IsFixedSize = true; + + // Previous orders + new GUILayoutGroup(new RectTransform(Vector2.One, parent: orderGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + CanBeFocused = false, + Stretch = false + }; + var soundIcons = new GUIFrame(new RectTransform(new Vector2(0.8f * iconRelativeWidth, 0.8f), layoutGroup.RectTransform), style: null) { CanBeFocused = false, @@ -602,11 +629,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(); } @@ -667,13 +694,11 @@ namespace Barotrauma #region Crew List Order Displayment - // TODO: CHECK ALL THE ORDER CONSTUCTOR CALLS - /// /// Sets the character's current order (if it's close enough to receive messages from orderGiver) and /// displays the order in the crew UI /// - public void SetCharacterOrder(Character character, Order order, string option, Character orderGiver) + public void SetCharacterOrder(Character character, Order order, string option, int priority, Character orderGiver) { if (order != null && order.TargetAllCharacters) { @@ -687,18 +712,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 +723,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) { @@ -739,7 +752,7 @@ namespace Barotrauma } else { - OrderChatMessage msg = new OrderChatMessage(order, "", order.IsReport ? hull : order.TargetEntity, null, orderGiver); + OrderChatMessage msg = new OrderChatMessage(order, "", priority, order.IsReport ? hull : order.TargetEntity, null, orderGiver); GameMain.Client?.SendChatMessage(msg); } } @@ -750,12 +763,12 @@ namespace Barotrauma if (IsSinglePlayer) { - character.SetOrder(order, option, orderGiver, speak: orderGiver != character); + character.SetOrder(order, option, priority, orderGiver, speak: orderGiver != character); orderGiver?.Speak(order?.GetChatMessage(character.Name, orderGiver.CurrentHull?.DisplayName, givingOrderToSelf: character == orderGiver, orderOption: option)); } else if (orderGiver != null) { - OrderChatMessage msg = new OrderChatMessage(order, option, order?.TargetSpatialEntity ?? order?.TargetItemComponent?.Item as ISpatialEntity, character, orderGiver); + OrderChatMessage msg = new OrderChatMessage(order, option, priority, order?.TargetSpatialEntity ?? order?.TargetItemComponent?.Item as ISpatialEntity, character, orderGiver); GameMain.Client?.SendChatMessage(msg); } } @@ -764,117 +777,193 @@ namespace Barotrauma /// /// Displays the specified order in the crew UI next to the character. /// - public void AddCurrentOrderIcon(Character character, Order order, string option) + public void AddCurrentOrderIcon(Character character, Order order, string option, int priority) { if (character == null) { return; } - var characterFrame = crewList.Content.GetChildByUserData(character); + var characterComponent = crewList.Content.GetChildByUserData(character); - if (characterFrame == null) { return; } + if (characterComponent == null) { return; } - GUILayoutGroup layoutGroup = (GUILayoutGroup)characterFrame.FindChild(c => c is GUILayoutGroup); + var currentOrderIconList = GetCurrentOrderIconList(characterComponent); + var currentOrderIcons = currentOrderIconList.Content.Children; + var iconsToRemove = new List(); + var newPreviousOrders = new List(); + bool updatedExistingIcon = false; - // Get the OrderInfo from the current order icon - var currentOrderIcon = GetCurrentOrderIcon(layoutGroup); - OrderInfo? currentOrderInfo = null; - if (currentOrderIcon?.UserData is OrderInfo) + foreach (var icon in currentOrderIcons) { - currentOrderInfo = (OrderInfo)currentOrderIcon.UserData; - // No need to recreate icons if the current order matches the new order - if (currentOrderInfo.Value.MatchesOrder(order, option)) { return; } + var orderInfo = (OrderInfo)icon.UserData; + var matchingOrder = character.GetCurrentOrder(orderInfo.Order, orderInfo.OrderOption); + if (!matchingOrder.HasValue) + { + iconsToRemove.Add(icon); + newPreviousOrders.Add(orderInfo); + } + else if (orderInfo.MatchesOrder(order, option)) + { + icon.UserData = new OrderInfo(order, option, priority); + updatedExistingIcon = true; + } } - - // Remove the current order icon - layoutGroup.RemoveChild(currentOrderIcon); + iconsToRemove.ForEach(c => currentOrderIconList.RemoveChild(c)); // Remove a previous order icon if it matches the new order // We don't want the same order as both a current order and a previous order - foreach (GUIComponent icon in GetPreviousOrderIcons(layoutGroup)) + var previousOrderIconGroup = GetPreviousOrderIconGroup(characterComponent); + var previousOrderIcons = previousOrderIconGroup.Children; + foreach (var icon in previousOrderIcons) { - if (icon?.UserData is OrderInfo info && info.MatchesOrder(order, option)) + var orderInfo = (OrderInfo)icon.UserData; + if (orderInfo.MatchesOrder(order, option)) { - layoutGroup.RemoveChild(icon); + previousOrderIconGroup.RemoveChild(icon); break; } } - // Create a new previous order icon from the current order icon's OrderInfo - if (currentOrderInfo.HasValue) + // Rearrange the icons before adding anything + if (updatedExistingIcon) { - AddPreviousOrderIcon(character, layoutGroup, currentOrderInfo.Value); + RearrangeIcons(); } - if (order == null || order.Identifier == dismissedOrderPrefab.Identifier) { return; } - - if (GetPreviousOrderIcons(layoutGroup).Count() >= MaxOrderIcons) + for (int i = newPreviousOrders.Count - 1; i >= 0; i--) { - RemoveLastPreviousOrderIcon(layoutGroup); + AddPreviousOrderIcon(character, characterComponent, newPreviousOrders[i]); } - var orderFrame = new GUIButton( - new RectTransform( - layoutGroup.GetChildByUserData("job").RectTransform.RelativeSize, - layoutGroup.RectTransform), - style: null) + if (order == null || order.Identifier == dismissedOrderPrefab.Identifier || updatedExistingIcon) { - UserData = new OrderInfo(order, option), - OnClicked = (button, userData) => - { - if (!CanIssueOrders) { return false; } - SetCharacterOrder(character, dismissedOrderPrefab, null, Character.Controlled); - return true; - } + currentOrderIconList.CanDragElements = currentOrderIconList.Content.CountChildren > 1; + RearrangeIcons(); + return; + } + + int orderIconCount = currentOrderIconList.Content.CountChildren + previousOrderIconGroup.CountChildren; + if (orderIconCount >= CharacterInfo.MaxCurrentOrders) + { + RemoveLastOrderIcon(characterComponent); + } + + float nodeWidth = ((1.0f / CharacterInfo.MaxCurrentOrders) * currentOrderIconList.Parent.Rect.Width) - ((CharacterInfo.MaxCurrentOrders - 1) * currentOrderIconList.Spacing); + Point size = new Point((int)nodeWidth, currentOrderIconList.RectTransform.NonScaledSize.Y); + var nodeIcon = CreateNodeIcon(size, currentOrderIconList.Content.RectTransform, GetOrderIconSprite(order, option), order.Color, tooltip: CreateOrderTooltip(order, option)); + nodeIcon.UserData = new OrderInfo(order, option, priority); + nodeIcon.OnSecondaryClicked = (image, userData) => + { + if (!CanIssueOrders) { return false; } + var orderInfo = (OrderInfo)userData; + SetCharacterOrder(character, dismissedOrderPrefab, Order.GetDismissOrderOption(orderInfo), + character.GetCurrentOrder(orderInfo.Order, orderInfo.OrderOption)?.ManualPriority ?? 0, + Character.Controlled); + return true; }; - CreateNodeIcon(orderFrame.RectTransform, order.SymbolSprite, order.Color, tooltip: order.Name); - - new GUIImage(new RectTransform(Vector2.One, orderFrame.RectTransform), cancelIcon, scaleToFit: true) + new GUIFrame(new RectTransform(new Point((int)(1.5f * nodeWidth)), parent: nodeIcon.RectTransform, Anchor.Center), "OuterGlowCircular") { CanBeFocused = false, - UserData = "cancel", + Color = order.Color, + UserData = "glow", Visible = false }; - orderFrame.RectTransform.RepositionChildInHierarchy(4); + int hierarchyIndex = GetOrderIconHierarchyIndex(priority); + if (hierarchyIndex != currentOrderIconList.Content.GetChildIndex(nodeIcon)) + { + nodeIcon.RectTransform.RepositionChildInHierarchy(hierarchyIndex); + } + + currentOrderIconList.CanDragElements = currentOrderIconList.Content.CountChildren > 1; + RearrangeIcons(); + + void RearrangeIcons() + { + if (character.CurrentOrders != null) + { + // Make sure priority values are up-to-date + foreach (var currentOrderInfo in character.CurrentOrders) + { + var component = currentOrderIconList.Content.FindChild(c => c?.UserData is OrderInfo componentOrderInfo && + componentOrderInfo.MatchesOrder(currentOrderInfo)); + if (component == null) { continue; } + var componentOrderInfo = (OrderInfo)component.UserData; + int newPriority = currentOrderInfo.ManualPriority; + if (componentOrderInfo.ManualPriority != newPriority) + { + component.UserData = new OrderInfo(componentOrderInfo, newPriority); + } + } + + currentOrderIconList.Content.RectTransform.SortChildren((x, y) => + { + var xOrder = (OrderInfo)x.GUIComponent.UserData; + var yOrder = (OrderInfo)y.GUIComponent.UserData; + return yOrder.ManualPriority.CompareTo(xOrder.ManualPriority); + }); + + if (currentOrderIconList.Parent is GUILayoutGroup parentGroup) + { + int iconCount = currentOrderIconList.Content.CountChildren; + float nonScaledWidth = ((float)iconCount / CharacterInfo.MaxCurrentOrders) * parentGroup.Rect.Width + (iconCount * currentOrderIconList.Spacing); + currentOrderIconList.RectTransform.NonScaledSize = new Point((int)nonScaledWidth, currentOrderIconList.RectTransform.NonScaledSize.Y); + parentGroup.Recalculate(); + previousOrderIconGroup.Recalculate(); + } + } + } + + static int GetOrderIconHierarchyIndex(int priority) + { + return CharacterInfo.HighestManualOrderPriority - priority; + } } - private void AddPreviousOrderIcon(Character character, GUILayoutGroup characterComponent, OrderInfo orderInfo) + public void AddCurrentOrderIcon(Character character, OrderInfo? orderInfo) + { + AddCurrentOrderIcon(character, orderInfo?.Order, orderInfo?.OrderOption, orderInfo?.ManualPriority ?? 0); + } + + private void AddPreviousOrderIcon(Character character, GUIComponent characterComponent, OrderInfo orderInfo) { if (orderInfo.Order == null || orderInfo.Order.Identifier == dismissedOrderPrefab.Identifier) { return; } - var maxPreviousOrderIcons = GetCurrentOrderIcon(characterComponent) != null ? MaxOrderIcons - 1 : MaxOrderIcons; - if (GetPreviousOrderIcons(characterComponent).Count() >= maxPreviousOrderIcons) + var currentOrderIconList = GetCurrentOrderIconList(characterComponent); + int maxPreviousOrderIcons = CharacterInfo.MaxCurrentOrders - currentOrderIconList.Content.CountChildren; + + if (maxPreviousOrderIcons < 1) { return; } + + var previousOrderIconGroup = GetPreviousOrderIconGroup(characterComponent); + if (previousOrderIconGroup.CountChildren >= maxPreviousOrderIcons) { - RemoveLastPreviousOrderIcon(characterComponent); + RemoveLastPreviousOrderIcon(previousOrderIconGroup); } - var previousOrderInfo = new OrderInfo(orderInfo); - - var prevOrderFrame = new GUIButton( - new RectTransform( - characterComponent.GetChildByUserData("job").RectTransform.RelativeSize, - characterComponent.RectTransform), - style: null) + float nodeWidth = ((1.0f / CharacterInfo.MaxCurrentOrders) * previousOrderIconGroup.Parent.Rect.Width) - ((CharacterInfo.MaxCurrentOrders - 1) * currentOrderIconList.Spacing); + Point size = new Point((int)nodeWidth, previousOrderIconGroup.Rect.Height); + var previousOrderInfo = new OrderInfo(orderInfo, OrderInfo.OrderType.Previous); + var prevOrderFrame = new GUIButton(new RectTransform(size, parent: previousOrderIconGroup.RectTransform), style: null) { - UserData = previousOrderInfo, + UserData = previousOrderInfo, OnClicked = (button, userData) => { if (!CanIssueOrders) { return false; } var orderInfo = (OrderInfo)userData; - SetCharacterOrder(character, orderInfo.Order, orderInfo.OrderOption, Character.Controlled); + SetCharacterOrder(character, orderInfo.Order, orderInfo.OrderOption, CharacterInfo.HighestManualOrderPriority, Character.Controlled); return true; } }; + prevOrderFrame.RectTransform.IsFixedSize = true; var prevOrderIconFrame = new GUIFrame( new RectTransform(new Vector2(0.8f), prevOrderFrame.RectTransform, anchor: Anchor.BottomLeft), style: null); - CreateNodeIcon( + CreateNodeIcon(Vector2.One, prevOrderIconFrame.RectTransform, - previousOrderInfo.Order.SymbolSprite, + GetOrderIconSprite(previousOrderInfo), previousOrderInfo.Order.Color, - tooltip: previousOrderInfo.Order.Name); + tooltip: CreateOrderTooltip(previousOrderInfo)); foreach (GUIComponent c in prevOrderIconFrame.Children) { @@ -891,22 +980,20 @@ namespace Barotrauma CanBeFocused = false }; - var positionInHierarchy = GetCurrentOrderIcon(characterComponent) != null ? 5 : 4; - prevOrderFrame.RectTransform.RepositionChildInHierarchy(positionInHierarchy); + prevOrderFrame.SetAsFirstChild(); } - private void AddOldPreviousOrderIcons(Character character, GUILayoutGroup oldCharacterComponent) + private void AddOldPreviousOrderIcons(Character character, GUIComponent oldCharacterComponent) { - var prevOrderIcons = GetPreviousOrderIcons(oldCharacterComponent).ToList(); - if (prevOrderIcons.None()) { return; } - if (prevOrderIcons.Count() > 1) + var oldPrevOrderIcons = GetPreviousOrderIconGroup(oldCharacterComponent).Children; + if (oldPrevOrderIcons.None()) { return; } + if (oldPrevOrderIcons.Count() > 1) { - prevOrderIcons.Sort((x, y) => oldCharacterComponent.GetChildIndex(x).CompareTo(oldCharacterComponent.GetChildIndex(y))); - prevOrderIcons.Reverse(); + oldPrevOrderIcons = oldPrevOrderIcons.Reverse(); } - if (crewList.Content.Children.FirstOrDefault(c => c?.UserData == character)?.GetChild() is GUILayoutGroup newCharacterComponent) + if (crewList.Content.Children.FirstOrDefault(c => c.UserData == character) is GUIComponent newCharacterComponent) { - foreach (GUIComponent icon in prevOrderIcons) + foreach (GUIComponent icon in oldPrevOrderIcons) { if (icon.UserData is OrderInfo orderInfo) { @@ -916,36 +1003,91 @@ namespace Barotrauma } } - private void RemoveLastPreviousOrderIcon(GUILayoutGroup characterComponent) + private void RemoveLastOrderIcon(GUIComponent characterComponent) { - var prevOrderIcons = GetPreviousOrderIcons(characterComponent); - if (prevOrderIcons.None()) { return; } - if (prevOrderIcons.Count() == 1) + var previousOrderIconGroup = GetPreviousOrderIconGroup(characterComponent); + if (RemoveLastPreviousOrderIcon(previousOrderIconGroup)) { - characterComponent.RemoveChild(prevOrderIcons.First()); + return; } - else + var currentOrderIconList = GetCurrentOrderIconList(characterComponent); + if (currentOrderIconList.Content.CountChildren > 0) { - int highestIndex = 0; - GUIComponent oldestPreviousOrderIcon = null; - foreach (GUIComponent icon in prevOrderIcons) - { - int i = characterComponent.GetChildIndex(icon); - if (i > highestIndex || oldestPreviousOrderIcon == null) - { - highestIndex = i; - oldestPreviousOrderIcon = icon; - } - } - characterComponent.RemoveChild(oldestPreviousOrderIcon); + var iconToRemove = currentOrderIconList.Content.Children.Last(); + currentOrderIconList.RemoveChild(iconToRemove); + return; } } - private GUIComponent GetCurrentOrderIcon(GUILayoutGroup characterComponent) => - characterComponent?.FindChild(c => c?.UserData is OrderInfo orderInfo && orderInfo.ComponentIdentifier == "currentorder"); + private bool RemoveLastPreviousOrderIcon(GUILayoutGroup iconGroup) + { + if (iconGroup.CountChildren > 0) + { + var iconToRemove = iconGroup.Children.Last(); + iconGroup.RemoveChild(iconToRemove); + return true; + } + return false; + } - private IEnumerable GetPreviousOrderIcons(GUILayoutGroup characterComponent) => - characterComponent?.FindChildren(c => c?.UserData is OrderInfo orderInfo && orderInfo.ComponentIdentifier == "previousorder"); + private GUIListBox GetCurrentOrderIconList(GUIComponent characterComponent) => + characterComponent?.GetChild().GetChild().GetChild(); + + private GUILayoutGroup GetPreviousOrderIconGroup(GUIComponent characterComponent) => + characterComponent?.GetChild().GetChild().GetChild(); + + private void OnOrdersRearranged(GUIListBox orderList, object userData) + { + var orderComponent = orderList.Content.GetChildByUserData(userData); + if (orderComponent == null) { return; } + var orderInfo = (OrderInfo)userData; + var priority = Math.Max(CharacterInfo.HighestManualOrderPriority - orderList.Content.GetChildIndex(orderComponent), 1); + if (orderInfo.ManualPriority == priority) { return; } + var character = (Character)orderList.UserData; + SetCharacterOrder(character, orderInfo.Order, orderInfo.OrderOption, priority, Character.Controlled); + } + + 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 @@ -982,22 +1124,8 @@ namespace Barotrauma public void CreateModerationContextMenu(Point mousePos, Client client) { - if (IsSinglePlayer || client == null || (GameMain.NetworkMember?.ConnectedClients?.All(match => match != client) ?? true)) { return; } - - contextMenu = new GUIFrame(new RectTransform(new Vector2(0.1f, 0.15f), GUI.Canvas) { ScreenSpaceOffset = mousePos }, style: "GUIToolTip") { UserData = client }; - - var nameLabel = new GUITextBlock(new RectTransform(new Vector2(1f, 0.2f), contextMenu.RectTransform), client.Name, font: GUI.SubHeadingFont) - { - Padding = new Vector4(8), - TextColor = client.Character?.Info?.Job.Prefab.UIColor ?? Color.White - }; - - var optionsList = new GUIListBox(new RectTransform(new Vector2(1f, 0.8f), contextMenu.RectTransform, Anchor.BottomLeft), style: null) - { - Padding = new Vector4(4, 0, 4, 4), - AutoHideScrollBar = false, - ScrollBarVisible = false - }; + if (GUIContextMenu.CurrentContextMenu != null) { return; } + if (IsSinglePlayer || client == null || (!GameMain.Client?.PreviouslyConnectedClients?.Contains(client) ?? true)) { return; } bool hasSteam = client.SteamID > 0 && SteamManager.IsInitialized, canKick = GameMain.Client.HasPermission(ClientPermissions.Kick), @@ -1010,193 +1138,82 @@ namespace Barotrauma canKick = canBan = canPromo = false; } - RectTransform parent = optionsList.Content.RectTransform; - new GUITextBlock(new RectTransform(Point.Zero, parent), TextManager.Get("viewsteamprofile"), font: GUI.SmallFont) - { - Padding = new Vector4(4), - Enabled = hasSteam, - UserData = "steam" - }; - - new GUITextBlock(new RectTransform(Point.Zero, parent), TextManager.Get("moderationmenu.userdetails"), font: GUI.SmallFont) - { - Padding = new Vector4(4), - Enabled = true, - UserData = "user" - }; - - new GUITextBlock(new RectTransform(Point.Zero, parent), TextManager.Get("permissions"), font: GUI.SmallFont) - { - Padding = new Vector4(4), - Enabled = canPromo, - UserData = "promote" - }; - - 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("ban"), font: GUI.SmallFont) - { - Padding = new Vector4(4), - Enabled = canBan, - UserData = "ban" - }; - - foreach (GUIComponent c in optionsList.Content.Children) - { - if (c is GUITextBlock child && !child.Enabled) - { - child.TextColor *= 0.5f; - } - } - - var children = optionsList.Content.Children.ToList(); - - // Resize all children to the size of their text - foreach (GUITextBlock block in children.Where(c => c is GUITextBlock).Cast()) - { - block.RectTransform.NonScaledSize = new Point((int) (block.TextSize.X + (block.Padding.X + block.Padding.Z)), (int)(18 * GUI.Scale)); - } - - int horizontalPadding = (int)(optionsList.Padding.X + optionsList.Padding.Z); - int verticalPadding = (int)(optionsList.Padding.Y + optionsList.Padding.W); - int largestWidth = children.Max(c => c.Rect.Width + horizontalPadding); - - // If the name is bigger than any of the options then overwrite - nameLabel.RectTransform.MinSize = new Point((int)(nameLabel.TextSize.X + (nameLabel.Padding.X + nameLabel.Padding.Z)), nameLabel.RectTransform.NonScaledSize.Y); - if (largestWidth < nameLabel.RectTransform.MinSize.X) { largestWidth = nameLabel.RectTransform.MinSize.X; } - - // Resize all children to the size of the longest element - foreach (GUIComponent c in children) { c.RectTransform.MinSize = new Point(largestWidth, c.Rect.Height); } + List options = new List(); - // crop the context menu - contextMenu.RectTransform.NonScaledSize = new Point(largestWidth, (children.Sum(c => c.Rect.Height) + verticalPadding) + nameLabel.Rect.Height); + options.Add(new ContextMenuOption("ViewSteamProfile", isEnabled: hasSteam, onSelected: delegate + { + Steamworks.SteamFriends.OpenWebOverlay($"https://steamcommunity.com/profiles/{client.SteamID}"); + })); - // if the menu would go off the screen then move it up - if (contextMenu.Rect.Bottom > GameMain.GraphicsHeight) + options.Add(new ContextMenuOption("ModerationMenu.UserDetails", isEnabled: true, onSelected: delegate { - contextMenu.RectTransform.ScreenSpaceOffset = new Point(mousePos.X, mousePos.Y - contextMenu.Rect.Height); - } - - optionsList.OnSelected = (component, obj) => + GameMain.NetLobbyScreen?.SelectPlayer(client); + })); + + + // Creates sub context menu options for all the ranks + List permissionOptions = new List(); + foreach (PermissionPreset rank in PermissionPreset.List) { - if (component.Enabled) + permissionOptions.Add(new ContextMenuOption(rank.Name, isEnabled: true, onSelected: () => { - switch (obj) + string label = TextManager.GetWithVariables(rank.Permissions == ClientPermissions.None ? "clearrankprompt" : "giverankprompt", new []{ "[user]", "[rank]" }, new []{ client.Name, rank.Name }); + GUIMessageBox msgBox = new GUIMessageBox(string.Empty, label, new[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }); + + msgBox.Buttons[0].OnClicked = delegate { - case "steam": - Steamworks.SteamFriends.OpenWebOverlay($"https://steamcommunity.com/profiles/{client.SteamID}"); - break; - case "mute": - client.MutedLocally = !client.MutedLocally; - break; - case "kick": - GameMain.Client?.CreateKickReasonPrompt(client.Name, false); - break; - case "votekick": - GameMain.Client?.VoteForKick(client); - break; - case "ban": - GameMain.Client?.CreateKickReasonPrompt(client.Name, true); - break; - case "user": - GameMain.NetLobbyScreen?.SelectPlayer(client); - break; - } - contextMenu = null; - return true; - } - return false; - }; - } - - private void CreatePromoteSubMenu(Point pos, Client client) - { - if (client == null ) { return; } - - subContextMenu = new GUIListBox(new RectTransform(new Vector2(0.1f, 0.1f), GUI.Canvas) { ScreenSpaceOffset = pos }, style: "GUIToolTip") - { - AutoHideScrollBar = false, - ScrollBarVisible = false - }; - - foreach (var rank in PermissionPreset.List) - { - new GUITextBlock(new RectTransform(Point.Zero, subContextMenu.Content.RectTransform), rank.Name, font: GUI.SmallFont) - { - ToolTip = rank.Description, - UserData = rank, - Padding = new Vector4(4) - }; - } - - var children = subContextMenu.Content.Children.ToList(); - - // Resize all children to the size of their text - foreach (GUITextBlock block in children.Where(c => c is GUITextBlock).Cast()) - { - block.RectTransform.NonScaledSize = new Point((int) (block.TextSize.X + (block.Padding.X + block.Padding.Z)), (int)(18 * GUI.Scale)); - } - - int horizontalPadding = (int)(subContextMenu.Padding.X + subContextMenu.Padding.Z); - int largestWidth = children.Max(c => c.Rect.Width + horizontalPadding); - - // Resize all children to the size of the longest element - foreach (GUIComponent c in children) { c.RectTransform.MinSize = new Point(largestWidth, c.Rect.Height); } - - // crop the context menu - subContextMenu.RectTransform.NonScaledSize = new Point(largestWidth, children.Sum(c => c.Rect.Height) + horizontalPadding); - - // if the menu would go off the screen then move it up - if (subContextMenu.Rect.Bottom > GameMain.GraphicsHeight) - { - subContextMenu.RectTransform.ScreenSpaceOffset = new Point(pos.X, pos.Y - subContextMenu.Rect.Height); - } - - subContextMenu.OnSelected = (component, obj) => - { - if (component.Enabled && obj is PermissionPreset preset) - { - var label = TextManager.GetWithVariables(preset.Permissions == ClientPermissions.None ? "clearrankprompt" : "giverankprompt", new []{ "[user]", "[rank]" }, new []{ client.Name, preset.Name }); - - var msgBox = new GUIMessageBox(string.Empty, label, new[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }); - - msgBox.Buttons[0].OnClicked = (yesBtn, userdata) => - { - client.SetPermissions(preset.Permissions, preset.PermittedCommands); + client.SetPermissions(rank.Permissions, rank.PermittedCommands); GameMain.Client.UpdateClientPermissions(client); msgBox.Close(); return true; }; - msgBox.Buttons[1].OnClicked = (_, userdata) => + msgBox.Buttons[1].OnClicked = delegate { msgBox.Close(); return true; }; - contextMenu = null; - subContextMenu = null; - return true; - } - return false; - }; - } + }) { Tooltip = rank.Description }); + } - private static bool IsMouseOnContextMenu(Rectangle rect) - { - Rectangle expandedRect = rect; - expandedRect.Inflate(20, 20); - return expandedRect.Contains(PlayerInput.MousePosition); + options.Add(new ContextMenuOption("Permissions", isEnabled: canPromo, options: permissionOptions.ToArray())); + + Color clientColor = client.Character?.Info?.Job.Prefab.UIColor ?? Color.White; + + if (GameMain.Client.ConnectedClients.Contains(client)) + { + options.Add(new ContextMenuOption(client.MutedLocally ? "Unmute" : "Mute", isEnabled: client.ID != GameMain.Client?.ID, onSelected: delegate + { + client.MutedLocally = !client.MutedLocally; + })); + + bool kickEnabled = client.ID != GameMain.Client?.ID && client.AllowKicking; + + // if the user can kick create a kick option else create the votekick option + ContextMenuOption kickOption; + if (canKick) + { + kickOption = new ContextMenuOption("Kick", isEnabled: kickEnabled, onSelected: delegate + { + GameMain.Client?.CreateKickReasonPrompt(client.Name, false); + }); + } + else + { + kickOption = new ContextMenuOption("VoteToKick", isEnabled: kickEnabled, onSelected: delegate + { + GameMain.Client?.VoteForKick(client); + }); + } + + options.Add(kickOption); + } + + options.Add(new ContextMenuOption("Ban", isEnabled: canBan, onSelected: delegate + { + GameMain.Client?.CreateKickReasonPrompt(client.Name, true); + })); + + GUIContextMenu.CreateContextMenu(null, client.Name, headerColor: clientColor, options.ToArray()); } #endregion @@ -1212,22 +1229,20 @@ namespace Barotrauma if (GameMain.GraphicsWidth != screenResolution.X || GameMain.GraphicsHeight != screenResolution.Y || prevUIScale != GUI.Scale) { - var previousCrewList = crewList; + var oldCrewList = crewList; InitProjectSpecific(); - foreach (GUIComponent c in previousCrewList.Content.Children) + foreach (GUIComponent oldCharacterComponent in oldCrewList.Content.Children) { - if (!(c.UserData is Character character) || character.IsDead || character.Removed) { continue; } + if (!(oldCharacterComponent.UserData is Character character) || character.IsDead || character.Removed) { continue; } AddCharacter(character); - AddOldPreviousOrderIcons(character, c.GetChild()); + AddOldPreviousOrderIcons(character, oldCharacterComponent); } } crewAreaWithButtons.Visible = !(GameMain.GameSession?.GameMode is CampaignMode campaign) || (!campaign.ForceMapUI && !campaign.ShowCampaignUI); guiFrame.AddToGUIUpdateList(); - contextMenu?.AddToGUIUpdateList(false, 1); - subContextMenu?.AddToGUIUpdateList(false, 1); } public void SelectNextCharacter() @@ -1293,43 +1308,6 @@ namespace Barotrauma SelectPreviousCharacter(); } } - - // context menu behavior - if (contextMenu != null) - { - var promote = contextMenu.GetChild()?.Content.GetChildByUserData("promote"); - - if (promote != null && promote.Enabled) - { - promote.ExternalHighlight = subContextMenu != null; - - if (GUI.IsMouseOn(promote)) - { - if (contextMenu.UserData is Client client && subContextMenu == null) - { - CreatePromoteSubMenu(new Point(promote.Rect.Right, promote.Rect.Y), client); - } - } - else if (subContextMenu != null && !IsMouseOnContextMenu(subContextMenu.Rect)) - { - subContextMenu = null; - } - } - else - { - subContextMenu = null; - } - - if (subContextMenu == null && !IsMouseOnContextMenu(contextMenu.Rect)) - { - contextMenu = null; - } - } - - if (contextMenu == null && subContextMenu != null) - { - subContextMenu = null; - } if (GUI.DisableHUD) { return; } @@ -1363,23 +1341,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 +1406,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 +1438,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; @@ -1549,27 +1532,28 @@ namespace Barotrauma { crewArea.Visible = characters.Count > 0 && CharacterHealth.OpenHealthWindow == null; - foreach (GUIComponent child in crewList.Content.Children) + foreach (GUIComponent characterComponent in crewList.Content.Children) { - if (child.UserData is Character character) + if (characterComponent.UserData is Character character) { - child.Visible = Character.Controlled == null || Character.Controlled.TeamID == character.TeamID; - if (child.Visible) + characterComponent.Visible = Character.Controlled == null || Character.Controlled.TeamID == character.TeamID; + if (characterComponent.Visible) { - if (character == Character.Controlled && child.State != GUIComponent.ComponentState.Selected) + if (character == Character.Controlled && characterComponent.State != GUIComponent.ComponentState.Selected) { crewList.Select(character, force: true); } - if (child.FindChild(c => c is GUILayoutGroup) is GUILayoutGroup layoutGroup) + if (character.AIController is HumanAIController controller) { - if (GetCurrentOrderIcon(layoutGroup) is GUIComponent orderButton && - orderButton.GetChildByUserData("colorsource") is GUIComponent orderIcon && - orderButton.GetChildByUserData("cancel") is GUIComponent cancelIcon) + OrderInfo? currentOrderInfo = controller.ObjectiveManager?.GetCurrentOrderInfo(); + if (currentOrderInfo.HasValue) { - cancelIcon.Visible = GUI.IsMouseOn(orderIcon); + SetHighlightedOrderIcon(characterComponent, currentOrderInfo.Value.Order?.Identifier, currentOrderInfo.Value.OrderOption); } - if (layoutGroup.GetChildByUserData("soundicons")? - .FindChild(c => c.UserData is Pair pair && pair.First == "soundicon") is GUIImage soundIcon) + } + if (characterComponent.GetChild().GetChildByUserData("soundicons") is GUIComponent soundIconParent) + { + if (soundIconParent.FindChild(c => c.UserData is Pair pair && pair.First == "soundicon") is GUIImage soundIcon) { VoipClient.UpdateVoiceIndicator(soundIcon, 0.0f, deltaTime); } @@ -1597,6 +1581,33 @@ namespace Barotrauma UpdateReports(); } + private void SetHighlightedOrderIcon(GUIComponent characterComponent, string orderIdentifier, string orderOption) + { + var currentOrderIconList = GetCurrentOrderIconList(characterComponent); + if (currentOrderIconList == null) { return; } + bool foundMatch = false; + foreach (var orderIcon in currentOrderIconList.Content.Children) + { + var glowComponent = orderIcon.GetChildByUserData("glow"); + if (glowComponent == null) { continue; } + if (foundMatch) + { + glowComponent.Visible = false; + continue; + } + var orderInfo = (OrderInfo)orderIcon.UserData; + foundMatch = orderInfo.MatchesOrder(orderIdentifier, orderOption); + glowComponent.Visible = foundMatch; + } + } + + public void SetHighlightedOrderIcon(Character character, string orderIdentifier, string orderOption) + { + if (crewList == null) { return; } + var characterComponent = crewList.Content.GetChildByUserData(character); + SetHighlightedOrderIcon(characterComponent, orderIdentifier, orderOption); + } + #endregion #region Command UI @@ -1947,7 +1958,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 +2043,7 @@ namespace Barotrauma SetCharacterTooltip(c, characterContext); } node.OnClicked = null; + node.OnSecondaryClicked = null; centerNode = node; } @@ -2042,6 +2059,7 @@ namespace Barotrauma c.ToolTip = TextManager.Get("commandui.return"); } node.OnClicked = NavigateBackward; + node.OnSecondaryClicked = null; returnNode = node; } @@ -2072,11 +2090,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 +2105,10 @@ namespace Barotrauma private void RemoveExtraOptionNodes() { - extraOptionNodes.ForEach(node => commandFrame.RemoveChild(node)); + if (commandFrame != null) + { + extraOptionNodes.ForEach(node => commandFrame.RemoveChild(node)); + } extraOptionNodes.Clear(); } @@ -2111,7 +2135,7 @@ namespace Barotrauma var tooltip = TextManager.Get("ordercategorytitle." + category.ToString().ToLower()); var categoryDescription = TextManager.Get("ordercategorydescription." + category.ToString(), true); if (!string.IsNullOrWhiteSpace(categoryDescription)) { tooltip += "\n" + categoryDescription; } - CreateNodeIcon(node.RectTransform, sprite.Item1, sprite.Item2, tooltip: tooltip); + CreateNodeIcon(Vector2.One, node.RectTransform, sprite.Item1, sprite.Item2, tooltip: tooltip); } CreateHotkeyIcon(node.RectTransform, hotkey % 10); optionNodes.Add(new Tuple(node, Keys.D0 + hotkey % 10)); @@ -2125,7 +2149,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 +2169,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 +2220,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 +2270,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 +2288,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 +2340,12 @@ namespace Barotrauma if (contextualOrders.None()) { orderIdentifier = "cleanupitems"; - if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier)) && AIObjectiveCleanupItems.IsValidTarget(itemContext, Character.Controlled)) + 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 +2361,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))) { @@ -2351,8 +2409,7 @@ namespace Barotrauma // Show 'dismiss' order only when there are crew members with active orders orderIdentifier = "dismissed"; - if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier)) && - characters.Any(c => c.CurrentOrder != null && !c.CurrentOrder.Identifier.Equals(orderIdentifier))) + if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier)) && characters.Any(c => !c.IsDismissed)) { contextualOrders.Add(Order.GetPrefab(orderIdentifier)); } @@ -2366,35 +2423,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 @@ -2403,7 +2431,8 @@ namespace Barotrauma if (Order.PrefabList.Any(o => o.TargetItems.Length > 0 && o.TargetItems.Contains(item.Prefab.Identifier))) { return true; } 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)) { 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 +2464,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; @@ -2465,14 +2494,20 @@ namespace Barotrauma o = new Order(o.Prefab, orderTargetEntity, orderTargetEntity.Components.FirstOrDefault(ic => ic.GetType() == order.ItemComponentType), orderGiver: order.OrderGiver); } var character = !o.TargetAllCharacters ? characterContext ?? GetCharacterForQuickAssignment(o) : null; - SetCharacterOrder(character, o, null, Character.Controlled); + SetCharacterOrder(character, o, null, CharacterInfo.HighestManualOrderPriority, Character.Controlled); DisableCommandUI(); } return true; }; - // TODO: Might need to edit the tooltip - var icon = CreateNodeIcon(node.RectTransform, order.SymbolSprite, order.Color, - tooltip: mustSetOptionOrTarget || characterContext != null ? order.Name : order.Name + + + if (CanOpenManualAssignment(node)) + { + node.OnSecondaryClicked = (button, _) => CreateAssignmentNodes(button); + } + var showAssignmentTooltip = !mustSetOptionOrTarget && characterContext == null && !order.MustManuallyAssign && !order.TargetAllCharacters; + var orderName = GetOrderNameBasedOnContextuality(order); + var icon = CreateNodeIcon(Vector2.One, node.RectTransform, order.SymbolSprite, order.Color, + 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 +2526,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 +2607,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, CharacterInfo.HighestManualOrderPriority, Character.Controlled); + DisableCommandUI(); + return true; + } + }; + if (CanOpenManualAssignment(optionButton)) + { + optionButton.OnSecondaryClicked = (button, _) => CreateAssignmentNodes(button); + } + optionNodes.Add(new Tuple(optionButton, Keys.None)); } } else @@ -2610,27 +2648,30 @@ namespace Barotrauma { UserData = userData, Font = GUI.SmallFont, - ToolTip = item?.Name ?? order.Name, + ToolTip = item?.Name ?? GetOrderNameBasedOnContextuality(order), OnClicked = (_, userData) => { if (!CanIssueOrders) { return false; } var o = userData as Tuple; - SetCharacterOrder(characterContext ?? GetCharacterForQuickAssignment(o.Item1), o.Item1, o.Item2, Character.Controlled); + SetCharacterOrder(characterContext ?? GetCharacterForQuickAssignment(o.Item1), o.Item1, o.Item2, CharacterInfo.HighestManualOrderPriority, Character.Controlled); DisableCommandUI(); 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) { icon = item.Prefab.MinimapIcon; } - var colorMultiplier = characters.Any(c => c.CurrentOrder != null && - c.CurrentOrder.Identifier == userData.Item1.Identifier && - c.CurrentOrder.TargetEntity == userData.Item1.TargetEntity) ? 0.5f : 1f; - CreateNodeIcon(optionElement.RectTransform, icon ?? order.SymbolSprite, order.Color * colorMultiplier); + var colorMultiplier = characters.Any(c => c.CurrentOrders.Any(o => o.Order != null && + o.Order.Identifier == userData.Item1.Identifier && + o.Order.TargetEntity == userData.Item1.TargetEntity)) ? 0.5f : 1f; + CreateNodeIcon(Vector2.One, optionElement.RectTransform, icon ?? order.SymbolSprite, order.Color * colorMultiplier); optionNodes.Add(new Tuple(optionElement, Keys.None)); } optionElements.Add(optionElement); @@ -2672,17 +2713,22 @@ namespace Barotrauma { if (!CanIssueOrders) { return false; } var o = userData as Tuple; - SetCharacterOrder(characterContext ?? GetCharacterForQuickAssignment(o.Item1), o.Item1, o.Item2, Character.Controlled); + SetCharacterOrder(characterContext ?? GetCharacterForQuickAssignment(o.Item1), o.Item1, o.Item2, CharacterInfo.HighestManualOrderPriority, Character.Controlled); DisableCommandUI(); 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)) { - icon = CreateNodeIcon(node.RectTransform, sprite, order.Color, + var showAssignmentTooltip = characterContext == null && !order.MustManuallyAssign && !order.TargetAllCharacters; + icon = CreateNodeIcon(Vector2.One, 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") + "\n" + (!PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.rightmouse") : TextManager.Get("input.leftmouse")) + ": " + TextManager.Get("commandui.manualassigntooltip")); @@ -2700,13 +2746,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)) { @@ -2742,7 +2788,7 @@ namespace Barotrauma }; if (order.Item1.Prefab.OptionSprites.TryGetValue(order.Item2, out Sprite sprite)) { - CreateNodeIcon(clickedOptionNode.RectTransform, sprite, order.Item1.Color, tooltip: order.Item2); + CreateNodeIcon(Vector2.One, clickedOptionNode.RectTransform, sprite, order.Item1.Color, tooltip: order.Item2); } SetCenterNode(clickedOptionNode); node = null; @@ -2791,7 +2837,7 @@ namespace Barotrauma CreateHotkeyIcon(returnNode.RectTransform, hotkey % 10, true); returnNodeHotkey = Keys.D0 + hotkey % 10; expandNodeHotkey = Keys.None; - return; + return true; } extraOptionCharacters.Clear(); @@ -2816,6 +2862,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) @@ -2864,7 +2911,7 @@ namespace Barotrauma OnClicked = (_, userData) => { if (!CanIssueOrders) { return false; } - SetCharacterOrder(userData as Character, order.Item1, order.Item2, Character.Controlled); + SetCharacterOrder(userData as Character, order.Item1, order.Item2, CharacterInfo.HighestManualOrderPriority, Character.Controlled); DisableCommandUI(); return true; } @@ -2874,12 +2921,13 @@ namespace Barotrauma var jobColor = character.Info?.Job?.Prefab?.UIColor ?? Color.White; // Order icon + var topOrderInfo = character.GetCurrentOrderWithTopPriority(); GUIImage orderIcon; - if (!character.IsDismissed) + if (topOrderInfo.HasValue) { - orderIcon = new GUIImage(new RectTransform(new Vector2(1.2f), node.RectTransform, anchor: Anchor.Center), character.CurrentOrder.SymbolSprite, scaleToFit: true); - var tooltip = character.CurrentOrder.Name; - if (!string.IsNullOrWhiteSpace(character.CurrentOrderOption)) { tooltip += " (" + character.CurrentOrder.GetOptionName(character.CurrentOrderOption) + ")"; }; + orderIcon = new GUIImage(new RectTransform(new Vector2(1.2f), node.RectTransform, anchor: Anchor.Center), topOrderInfo.Value.Order.SymbolSprite, scaleToFit: true); + var tooltip = topOrderInfo.Value.Order.Name; + if (!string.IsNullOrWhiteSpace(topOrderInfo.Value.OrderOption)) { tooltip += " (" + topOrderInfo.Value.Order.GetOptionName(topOrderInfo.Value.OrderOption) + ")"; }; orderIcon.ToolTip = tooltip; } else @@ -2944,11 +2992,31 @@ namespace Barotrauma } } - private GUIImage CreateNodeIcon(RectTransform parent, Sprite sprite, Color color, string tooltip = null) + private GUIImage CreateNodeIcon(Vector2 relativeSize, RectTransform parent, Sprite sprite, Color color, string tooltip = null) { // Icon return new GUIImage( - new RectTransform(Vector2.One, parent), + new RectTransform(relativeSize, parent), + sprite, + scaleToFit: true) + { + Color = color * nodeColorMultiplier, + HoverColor = color, + PressedColor = color, + SelectedColor = color, + ToolTip = tooltip, + UserData = "colorsource" + }; + } + + /// + /// Create node icon with a fixed absolute size + /// + private GUIImage CreateNodeIcon(Point absoluteSize, RectTransform parent, Sprite sprite, Color color, string tooltip = null) + { + // Icon + return new GUIImage( + new RectTransform(absoluteSize, parent: parent) { IsFixedSize = true }, sprite, scaleToFit: true) { @@ -3083,7 +3151,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 +3173,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) { @@ -3139,13 +3229,15 @@ namespace Barotrauma // 1. Prioritize those who are on the same submarine than the controlled character .OrderByDescending(c => Character.Controlled == null || c.Submarine == Character.Controlled.Submarine) // 2. Prioritize those who are already ordered to operate the item target of the new 'operate' order, or given the same maintenance order as now issued - .ThenByDescending(c => c.CurrentOrder != null && c.CurrentOrder.Identifier == order.Identifier && (order.Category == OrderCategory.Maintenance || (order.Category == OrderCategory.Operate && c.CurrentOrder.TargetSpatialEntity == order.TargetSpatialEntity))) + .ThenByDescending(c => c.CurrentOrders.Any(o => + o.Order != null && o.Order.Identifier == order.Identifier && + (order.Category == OrderCategory.Maintenance || (order.Category == OrderCategory.Operate && o.Order.TargetSpatialEntity == order.TargetSpatialEntity)))) // 3. Prioritize those with the appropriate job for the order .ThenByDescending(c => order.HasAppropriateJob(c)) // 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)); } @@ -3231,6 +3323,7 @@ namespace Barotrauma characters.Clear(); crewList.ClearChildren(); + GUIContextMenu.CurrentContextMenu = null; } public void Reset() diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs index c29550dcf..0aafbeb48 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs @@ -59,13 +59,16 @@ namespace Barotrauma public override void ShowStartMessage() { - if (Mission == null) return; - - new GUIMessageBox(Mission.Name, Mission.Description, new string[0], type: GUIMessageBox.Type.InGame, icon: Mission.Prefab.Icon) + foreach (Mission mission in Missions) { - IconColor = Mission.Prefab.IconColor, - UserData = "missionstartmessage" - }; + new GUIMessageBox( + mission.Prefab.IsSideObjective ? TextManager.AddPunctuation(':', TextManager.Get("sideobjective"), mission.Name) : mission.Name, + mission.Description, new string[0], type: GUIMessageBox.Type.InGame, icon: mission.Prefab.Icon) + { + IconColor = mission.Prefab.IconColor, + UserData = "missionstartmessage" + }; + } } /// @@ -158,7 +161,8 @@ namespace Barotrauma case TransitionType.ProgressToNextEmptyLocation: if (Level.Loaded.EndOutpost == null || !Level.Loaded.EndOutpost.DockedTo.Contains(leavingSub)) { - buttonText = TextManager.GetWithVariable("EnterLocation", "[locationname]", Level.Loaded.EndLocation?.Name ?? "[ERROR]"); + string textTag = availableTransition == TransitionType.ProgressToNextLocation ? "EnterLocation" : "EnterEmptyLocation"; + buttonText = TextManager.GetWithVariable(textTag, "[locationname]", Level.Loaded.EndLocation?.Name ?? "[ERROR]"); endRoundButton.Visible = !ForceMapUI && !ShowCampaignUI; } break; @@ -170,7 +174,8 @@ namespace Barotrauma case TransitionType.ReturnToPreviousEmptyLocation: if (Level.Loaded.StartOutpost == null || !Level.Loaded.StartOutpost.DockedTo.Contains(leavingSub)) { - buttonText = TextManager.GetWithVariable("EnterLocation", "[locationname]", Level.Loaded.StartLocation?.Name ?? "[ERROR]"); + string textTag = availableTransition == TransitionType.ReturnToPreviousLocation ? "EnterLocation" : "EnterEmptyLocation"; + buttonText = TextManager.GetWithVariable(textTag, "[locationname]", Level.Loaded.StartLocation?.Name ?? "[ERROR]"); endRoundButton.Visible = !ForceMapUI && !ShowCampaignUI; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index 8abd2ea9f..2d7d0d0e7 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(); } @@ -711,13 +711,20 @@ namespace Barotrauma DebugConsole.ThrowError($"Error when receiving campaign data from the server: mission prefab \"{availableMission.First}\" not found."); continue; } - if (availableMission.Second < 0 || availableMission.Second >= campaign.Map.CurrentLocation.Connections.Count) + if (availableMission.Second == 255) { - DebugConsole.ThrowError($"Error when receiving campaign data from the server: connection index for mission \"{availableMission.First}\" out of range (index: {availableMission.Second}, current location: {campaign.Map.CurrentLocation.Name}, connections: {campaign.Map.CurrentLocation.Connections.Count})."); - continue; + campaign.Map.CurrentLocation.UnlockMission(missionPrefab); + } + else + { + if (availableMission.Second < 0 || availableMission.Second >= campaign.Map.CurrentLocation.Connections.Count) + { + DebugConsole.ThrowError($"Error when receiving campaign data from the server: connection index for mission \"{availableMission.First}\" out of range (index: {availableMission.Second}, current location: {campaign.Map.CurrentLocation.Name}, connections: {campaign.Map.CurrentLocation.Connections.Count})."); + continue; + } + LocationConnection connection = campaign.Map.CurrentLocation.Connections[availableMission.Second]; + campaign.Map.CurrentLocation.UnlockMission(missionPrefab, connection); } - LocationConnection connection = campaign.Map.CurrentLocation.Connections[availableMission.Second]; - campaign.Map.CurrentLocation.UnlockMission(missionPrefab, connection); } GameMain.NetLobbyScreen.ToggleCampaignMode(true); @@ -812,8 +819,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..8b2c4fd35 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; } @@ -20,7 +22,7 @@ namespace Barotrauma { if (CoroutineManager.IsCoroutineRunning("LevelTransition") || CoroutineManager.IsCoroutineRunning("SubmarineTransition") || gameOver) { return; } - if (PlayerInput.RightButtonClicked() || + if (PlayerInput.SecondaryMouseButtonClicked() || PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.Escape)) { ShowCampaignUI = false; @@ -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; @@ -368,9 +372,6 @@ namespace Barotrauma SoundPlayer.OverrideMusicDuration = 18.0f; crewDead = false; - LevelData lvlData = GameMain.GameSession.LevelData; - bool beaconActive = GameMain.GameSession.Level.CheckBeaconActive(); - GameMain.GameSession.EndRound("", traitorResults, transitionType); var continueButton = GameMain.GameSession.RoundSummary?.ContinueButton; RoundSummary roundSummary = null; @@ -455,8 +456,6 @@ namespace Barotrauma } } - lvlData.IsBeaconActive = beaconActive; - SaveUtil.SaveGame(GameMain.GameSession.SavePath); } else @@ -526,6 +525,8 @@ namespace Barotrauma if (CoroutineManager.IsCoroutineRunning("LevelTransition") || CoroutineManager.IsCoroutineRunning("SubmarineTransition") || gameOver) { return; } base.Update(deltaTime); + + Map?.Radiation.UpdateRadiation(deltaTime); if (PlayerInput.SecondaryMouseButtonClicked() || PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.Escape)) @@ -699,6 +700,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/TestGameMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/TestGameMode.cs index 795c18836..2e4910a85 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/TestGameMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/TestGameMode.cs @@ -39,6 +39,10 @@ namespace Barotrauma base.Start(); CrewManager.InitSinglePlayerRound(); + foreach (Submarine submarine in Submarine.Loaded) + { + submarine.NeutralizeBallast(); + } if (SpawnOutpost) { 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..b918275ac 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; @@ -283,7 +283,7 @@ namespace Barotrauma.Tutorials doctor.RemoveActiveObjectiveEntity(patient1); TriggerTutorialSegment(3, GameMain.Config.KeyBindText(InputType.Command)); // Get the patient to medbay - while (patient1.CurrentOrder == null || patient1.CurrentOrder.Identifier != "follow") + while (patient1.GetCurrentOrderWithTopPriority()?.Order?.Identifier != "follow") { // TODO: Rework order highlighting for new command UI // GameMain.GameSession.CrewManager.HighlightOrderButton(patient1, "follow", highlightColor, new Vector2(5, 5)); @@ -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..2a68e6611 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); @@ -317,7 +317,8 @@ namespace Barotrauma.Tutorials yield return new WaitForSeconds(2f, false); TriggerTutorialSegment(4, GameMain.Config.KeyBindText(InputType.Select), GameMain.Config.KeyBindText(InputType.Shoot), GameMain.Config.KeyBindText(InputType.Deselect)); // Kill hammerhead officer_hammerhead = SpawnMonster("hammerhead", officer_hammerheadSpawnPos); - ((EnemyAIController)officer_hammerhead.AIController).StayInsideLevel = false; + officer_hammerhead.Params.AI.AvoidAbyss = false; + officer_hammerhead.Params.AI.StayInAbyss = false; officer_hammerhead.AIController.SelectTarget(officer.AiTarget); SetHighlight(officer_coilgunPeriscope, true); float originalDistance = Vector2.Distance(officer_coilgunPeriscope.WorldPosition, officer_hammerheadSpawnPos); @@ -371,12 +372,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 +384,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 +397,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 +424,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..221fc5d0a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs @@ -62,7 +62,7 @@ namespace Barotrauma.Tutorials yield return CoroutineStatus.Running; - GameMain.GameSession = new GameSession(subInfo, GameModePreset.Tutorial, missionPrefab: null); + GameMain.GameSession = new GameSession(subInfo, GameModePreset.Tutorial, missionPrefabs: null); (GameMain.GameSession.GameMode as TutorialMode).Tutorial = this; if (generationParams != null) @@ -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); @@ -182,7 +182,8 @@ namespace Barotrauma.Tutorials protected bool HasOrder(Character character, string identifier, string option = null) { - if (character.CurrentOrder?.Identifier == identifier) + var currentOrderInfo = character.GetCurrentOrderWithTopPriority(); + if (currentOrderInfo?.Order?.Identifier == identifier) { if (option == null) { @@ -190,8 +191,7 @@ namespace Barotrauma.Tutorials } else { - HumanAIController humanAI = character.AIController as HumanAIController; - return humanAI.CurrentOrderOption == option; + return currentOrderInfo?.OrderOption == option; } } 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 5894eb5fb..bb01da050 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); @@ -46,6 +48,7 @@ namespace Barotrauma msgBox.Buttons[0].OnClicked = delegate { msgBox.Close(); + if (GameMain.Client == null) { return true; } SendState(ReadyStatus.Yes); CreateResultsMessage(); return true; @@ -55,6 +58,7 @@ namespace Barotrauma msgBox.Buttons[1].OnClicked = delegate { msgBox.Close(); + if (GameMain.Client == null) { return true; } SendState(ReadyStatus.No); CreateResultsMessage(); return true; @@ -63,6 +67,8 @@ namespace Barotrauma private void CreateResultsMessage() { + if (GameMain.Client == null) { return; } + Vector2 relativeSize = new Vector2(0.2f, 0.3f); Point minSize = new Point(300, 400); resultsBox = new GUIMessageBox(readyCheckHeader, string.Empty, new[] { closeButton }, relativeSize, minSize, type: GUIMessageBox.Type.Vote) { UserData = ResultData, Draggable = true }; @@ -73,7 +79,7 @@ namespace Barotrauma GUIListBox listBox = new GUIListBox(new RectTransform(new Vector2(1f, 0.8f), resultsBox.Content.RectTransform)) { UserData = UserListData }; - foreach (var (id, status) in Clients) + foreach (var (id, _) in Clients) { Client? client = GameMain.Client.ConnectedClients.FirstOrDefault(c => c.ID == id); GUIFrame container = new GUIFrame(new RectTransform(new Vector2(1f, 0.15f), listBox.Content.RectTransform), style: "ListBoxElement") { UserData = id }; @@ -120,7 +126,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; } } @@ -130,12 +139,20 @@ namespace Barotrauma ReadyCheckState state = (ReadyCheckState) inc.ReadByte(); CrewManager? crewManager = GameMain.GameSession?.CrewManager; List otherClients = GameMain.Client.ConnectedClients; - if (crewManager == null || otherClients == null) { return; } + if (crewManager == null || otherClients == null) + { + if (state == ReadyCheckState.Start) + { + SendState(ReadyStatus.No); + } + return; + } switch (state) { case ReadyCheckState.Start: bool isOwn = false; + byte authorId = 0; float duration = inc.ReadSingle(); string author = inc.ReadString(); @@ -143,7 +160,8 @@ namespace Barotrauma if (hasAuthor) { - isOwn = inc.ReadByte() == GameMain.Client.ID; + authorId = inc.ReadByte(); + isOwn = authorId == GameMain.Client.ID; } ushort clientCount = inc.ReadUInt16(); @@ -165,12 +183,21 @@ namespace Barotrauma { rCheck.CreateMessageBox(author); } + + if (hasAuthor && rCheck.Clients.ContainsKey(authorId)) + { + rCheck.Clients[authorId] = ReadyStatus.Yes; + } break; case ReadyCheckState.Update: - crewManager.ActiveReadyCheck.time = inc.ReadSingle(); + float time = inc.ReadSingle(); ReadyStatus newState = (ReadyStatus) inc.ReadByte(); byte targetId = inc.ReadByte(); - crewManager.ActiveReadyCheck?.UpdateState(targetId, newState); + if (crewManager.ActiveReadyCheck != null) + { + crewManager.ActiveReadyCheck.time = time; + crewManager.ActiveReadyCheck?.UpdateState(targetId, newState); + } break; case ReadyCheckState.End: ushort count = inc.ReadUInt16(); @@ -190,6 +217,9 @@ namespace Barotrauma partial void EndReadyCheck() { + if (IsFinished) { return; } + IsFinished = true; + int readyCount = Clients.Count(pair => pair.Value == ReadyStatus.Yes); int totalCount = Clients.Count; GameMain.Client.AddChatMessage(ChatMessage.Create(string.Empty, readyCheckStatus(readyCount, totalCount), ChatMessageType.Server, null)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs index 135e61b32..d0a2be894 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs @@ -16,7 +16,7 @@ namespace Barotrauma private int jobColumnWidth, characterColumnWidth, statusColumnWidth; private readonly SubmarineInfo sub; - private readonly Mission selectedMission; + private readonly List selectedMissions; private readonly Location startLocation, endLocation; private readonly GameMode gameMode; @@ -32,11 +32,11 @@ namespace Barotrauma - public RoundSummary(SubmarineInfo sub, GameMode gameMode, Mission selectedMission, Location startLocation, Location endLocation) + public RoundSummary(SubmarineInfo sub, GameMode gameMode, IEnumerable selectedMissions, Location startLocation, Location endLocation) { this.sub = sub; this.gameMode = gameMode; - this.selectedMission = selectedMission; + this.selectedMissions = selectedMissions.ToList(); this.startLocation = startLocation; this.endLocation = endLocation; initialLocationReputation = startLocation?.Reputation?.Value ?? 0.0f; @@ -75,7 +75,7 @@ namespace Barotrauma //crew panel ------------------------------------------------------------------------------- - GUIFrame crewFrame = new GUIFrame(new RectTransform(new Vector2(0.35f, 0.55f), background.RectTransform, Anchor.TopCenter, minSize: new Point(minWidth, minHeight))); + GUIFrame crewFrame = new GUIFrame(new RectTransform(new Vector2(0.35f, 0.45f), background.RectTransform, Anchor.TopCenter, minSize: new Point(minWidth, minHeight))); GUIFrame crewFrameInner = new GUIFrame(new RectTransform(new Point(crewFrame.Rect.Width - padding * 2, crewFrame.Rect.Height - padding * 2), crewFrame.RectTransform, Anchor.Center), style: "InnerFrame"); var crewContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), crewFrameInner.RectTransform, Anchor.Center)) @@ -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) + if (gameSession.Missions.Any(m => m 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 ------------------------------------------------------------------------------- @@ -200,7 +200,7 @@ namespace Barotrauma GUIListBox reputationList = new GUIListBox(new RectTransform(Vector2.One, reputationContent.RectTransform)) { - Padding = new Vector4(2, 5, 0, 0) + Padding = new Vector4(4, 10, 0, 0) * GUI.Scale }; reputationList.ContentBackground.Color = Color.Transparent; @@ -253,103 +253,122 @@ namespace Barotrauma //mission panel ------------------------------------------------------------------------------- - GUIFrame missionframe = new GUIFrame(new RectTransform(new Vector2(0.39f, 0.22f), background.RectTransform, Anchor.TopCenter, minSize: new Point(minWidth, minHeight / 4))); - GUIFrame missionframeInner = new GUIFrame(new RectTransform(new Point(missionframe.Rect.Width - padding * 2, missionframe.Rect.Height - padding * 2), missionframe.RectTransform, Anchor.Center), style: "InnerFrame"); - - var missionContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), missionframeInner.RectTransform, Anchor.Center)) + GUIFrame missionframe = new GUIFrame(new RectTransform(new Vector2(0.39f, 0.3f), background.RectTransform, Anchor.TopCenter, minSize: new Point(minWidth, minHeight / 4))); + GUILayoutGroup missionFrameContent = new GUILayoutGroup(new RectTransform(new Point(missionframe.Rect.Width - padding * 2, missionframe.Rect.Height - padding * 2), missionframe.RectTransform, Anchor.Center)) + { + Stretch = true, + RelativeSpacing = 0.05f + }; + GUIFrame missionframeInner = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.9f), missionFrameContent.RectTransform, Anchor.Center), style: "InnerFrame"); + + var missionContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.93f), missionframeInner.RectTransform, Anchor.Center)) { - RelativeSpacing = 0.05f, Stretch = true }; + List missionsToDisplay = new List(selectedMissions); + if (!selectedMissions.Any() && startLocation?.SelectedMission != null) { missionsToDisplay.Add(startLocation.SelectedMission); } + + if (missionsToDisplay.Any()) + { + var missionHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionContent.RectTransform), + TextManager.Get(missionsToDisplay.Count > 1 ? "Missions" : "Mission"), textAlignment: Alignment.TopLeft, font: GUI.SubHeadingFont); + missionHeader.RectTransform.MinSize = new Point(0, (int)(missionHeader.Rect.Height * 1.2f)); + } + + GUIListBox missionList = new GUIListBox(new RectTransform(Vector2.One, missionContent.RectTransform, Anchor.Center)) + { + Padding = new Vector4(4, 10, 0, 0) * GUI.Scale + }; + missionList.ContentBackground.Color = Color.Transparent; + + ButtonArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), missionFrameContent.RectTransform, Anchor.BottomCenter), isHorizontal: true, childAnchor: Anchor.BottomRight) + { + RelativeSpacing = 0.025f + }; + + missionFrameContent.Recalculate(); + missionContent.Recalculate(); + if (!string.IsNullOrWhiteSpace(endMessage)) { - var endText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionContent.RectTransform), - TextManager.GetServerMessage(endMessage), wrap: true); + var endText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionList.Content.RectTransform), + TextManager.GetServerMessage(endMessage), wrap: true) + { + CanBeFocused = false + }; endText.RectTransform.MinSize = new Point(0, endText.Rect.Height); - var line = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.1f), missionContent.RectTransform), style: "HorizontalLine"); + var line = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.1f), missionList.Content.RectTransform), style: "HorizontalLine"); line.RectTransform.NonScaledSize = new Point(line.Rect.Width, GUI.IntScale(5.0f)); } - var missionContentHorizontal = new GUILayoutGroup(new RectTransform(Vector2.One, missionContent.RectTransform), childAnchor: Anchor.TopLeft, isHorizontal: true) + foreach (Mission displayedMission in missionsToDisplay) { - RelativeSpacing = 0.025f, - Stretch = true - }; + var missionContentHorizontal = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.8f), missionList.Content.RectTransform), childAnchor: Anchor.CenterLeft, isHorizontal: true) + { + RelativeSpacing = 0.025f, + Stretch = true + }; - Mission displayedMission = selectedMission ?? startLocation.SelectedMission; - string missionMessage = ""; - GUIImage missionIcon; - if (displayedMission != null) - { - missionMessage = - displayedMission == selectedMission ? - displayedMission.Completed ? displayedMission.SuccessMessage : displayedMission.FailureMessage : + string missionMessage = + selectedMissions.Contains(displayedMission) ? + displayedMission.Completed ? displayedMission.SuccessMessage : displayedMission.FailureMessage : displayedMission.Description; - missionIcon = new GUIImage(new RectTransform(new Point(missionContentHorizontal.Rect.Height), missionContentHorizontal.RectTransform), displayedMission.Prefab.Icon, scaleToFit: true) + GUIImage missionIcon = new GUIImage(new RectTransform(new Point((int)(missionContentHorizontal.Rect.Height)), missionContentHorizontal.RectTransform), displayedMission.Prefab.Icon, scaleToFit: true) { Color = displayedMission.Prefab.IconColor - }; - if (displayedMission == selectedMission) + }; + missionIcon.RectTransform.MinSize = new Point((int)(missionContentHorizontal.Rect.Height * 0.9f)); + if (selectedMissions.Contains(displayedMission)) { - new GUIImage(new RectTransform(Vector2.One, missionIcon.RectTransform), displayedMission.Completed ? "MissionCompletedIcon" : "MissionFailedIcon", scaleToFit: true); + new GUIImage(new RectTransform(Vector2.One, missionIcon.RectTransform), displayedMission.Completed ? "MissionCompletedIcon" : "MissionFailedIcon", scaleToFit: true); } - } - else - { - missionIcon = new GUIImage(new RectTransform(new Point(missionContentHorizontal.Rect.Height), missionContentHorizontal.RectTransform), style: "NoMissionIcon", scaleToFit: true); - } - var missionTextContent = new GUILayoutGroup(new RectTransform(Vector2.One, missionContentHorizontal.RectTransform)) - { - RelativeSpacing = 0.05f - }; - missionContentHorizontal.Recalculate(); - missionContent.Recalculate(); - missionIcon.RectTransform.MinSize = new Point(0, missionContentHorizontal.Rect.Height); - missionTextContent.RectTransform.MaxSize = new Point(int.MaxValue, missionIcon.Rect.Width); - GUITextBlock missionDescription = null; - if (displayedMission == null) - { + var missionTextContent = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.8f), missionContentHorizontal.RectTransform)) + { + RelativeSpacing = 0.05f + }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), - TextManager.Get("nomission"), font: GUI.LargeFont); - } - else - { + displayedMission.Name, font: GUI.SubHeadingFont); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), - TextManager.AddPunctuation(':', TextManager.Get("Mission"), displayedMission.Name), font: GUI.SubHeadingFont); - missionDescription = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), missionMessage, wrap: true); - if (displayedMission == selectedMission && displayedMission.Completed) + if (selectedMissions.Contains(displayedMission) && displayedMission.Completed && displayedMission.Reward > 0) { string rewardText = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", displayedMission.Reward)); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), TextManager.GetWithVariable("MissionReward", "[reward]", rewardText)); } + + if (displayedMission != missionsToDisplay.Last()) + { + var spacing = new GUIFrame(new RectTransform(new Vector2(1.0f, 1.0f), missionList.Content.RectTransform) { MaxSize = new Point(int.MaxValue, GUI.IntScale(15)) }, style: null); + new GUIFrame(new RectTransform(new Vector2(0.8f, 1.0f), spacing.RectTransform, Anchor.Center) { RelativeOffset = new Vector2(0.1f, 0.0f) }, "HorizontalLine"); + } } - ButtonArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), missionContent.RectTransform, Anchor.BottomCenter), isHorizontal: true, childAnchor: Anchor.BottomRight) + if (!missionsToDisplay.Any()) { - IgnoreLayoutGroups = true, - RelativeSpacing = 0.025f - }; + var missionContentHorizontal = new GUILayoutGroup(new RectTransform(Vector2.One, missionList.Content.RectTransform), childAnchor: Anchor.TopLeft, isHorizontal: true) + { + RelativeSpacing = 0.025f, + Stretch = true + }; + GUIImage missionIcon = new GUIImage(new RectTransform(new Point((int)(missionContentHorizontal.Rect.Height * 0.7f)), missionContentHorizontal.RectTransform), style: "NoMissionIcon", scaleToFit: true); + missionIcon.RectTransform.MinSize = new Point((int)(missionContentHorizontal.Rect.Height * 0.7f)); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionContentHorizontal.RectTransform), + TextManager.Get("nomission"), font: GUI.LargeFont); + } + + /*missionContentHorizontal.Recalculate(); + missionContent.Recalculate(); + missionIcon.RectTransform.MinSize = new Point(0, missionContentHorizontal.Rect.Height); + missionTextContent.RectTransform.MaxSize = new Point(int.MaxValue, missionIcon.Rect.Width);*/ ContinueButton = new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), ButtonArea.RectTransform), TextManager.Get("Close")); ButtonArea.RectTransform.NonScaledSize = new Point(ButtonArea.Rect.Width, ContinueButton.Rect.Height); ButtonArea.RectTransform.IsFixedSize = true; - missionContent.Recalculate(); - //description overlapping with the buttons -> switch to small font - if (missionDescription != null && missionDescription.Rect.Y + missionDescription.TextSize.Y > ButtonArea.Rect.Y) - { - missionDescription.Font = GUI.Style.SmallFont; - //still overlapping -> shorten the text - if (missionDescription.Rect.Y + missionDescription.TextSize.Y > ButtonArea.Rect.Y && missionDescription.WrappedText.Contains('\n')) - { - missionDescription.ToolTip = missionDescription.Text; - missionDescription.Text = missionDescription.WrappedText.Split('\n').First() + "..."; - } - } + missionFrameContent.Recalculate(); // set layout ------------------------------------------------------------------- @@ -396,15 +415,21 @@ namespace Barotrauma textTag = "RoundSummaryLeaving"; break; case CampaignMode.TransitionType.ProgressToNextLocation: - case CampaignMode.TransitionType.ProgressToNextEmptyLocation: locationName = endLocation?.Name; textTag = "RoundSummaryProgress"; break; + case CampaignMode.TransitionType.ProgressToNextEmptyLocation: + locationName = endLocation?.Name; + textTag = "RoundSummaryProgressToEmptyLocation"; + break; case CampaignMode.TransitionType.ReturnToPreviousLocation: - case CampaignMode.TransitionType.ReturnToPreviousEmptyLocation: locationName = startLocation?.Name; textTag = "RoundSummaryReturn"; break; + case CampaignMode.TransitionType.ReturnToPreviousEmptyLocation: + locationName = startLocation?.Name; + textTag = "RoundSummaryReturnToEmptyLocation"; + break; default: textTag = Submarine.MainSub.AtEndPosition ? "RoundSummaryProgress" : "RoundSummaryReturn"; break; @@ -456,7 +481,7 @@ namespace Barotrauma GUIListBox crewList = new GUIListBox(new RectTransform(Vector2.One, parent.RectTransform)) { - Padding = new Vector4(2, 5, 0, 0), + Padding = new Vector4(4, 10, 0, 0) * GUI.Scale, AutoHideScrollBar = false }; crewList.ContentBackground.Color = Color.Transparent; @@ -503,7 +528,7 @@ namespace Barotrauma Character character = characterInfo.Character; if (character == null || character.IsDead) { - if (character == null && characterInfo.IsNewHire) + if (character == null && characterInfo.IsNewHire && characterInfo.CauseOfDeath == null) { statusText = TextManager.Get("CampaignCrew.NewHire"); statusColor = GUI.Style.Blue; @@ -551,7 +576,7 @@ namespace Barotrauma string name, float reputation, float normalizedReputation, float initialReputation, string shortDescription, string fullDescription, Sprite icon, Sprite backgroundPortrait, Color iconColor) { - var factionFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.3f), parent.RectTransform), style: null); + var factionFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.35f), parent.RectTransform), style: null); if (backgroundPortrait != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs index 80e58d4ae..0a9b4eca2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs @@ -65,10 +65,11 @@ namespace Barotrauma keyMapping[(int)InputType.Voice] = new KeyOrMouse(Keys.V); keyMapping[(int)InputType.LocalVoice] = new KeyOrMouse(Keys.B); keyMapping[(int)InputType.Command] = new KeyOrMouse(MouseButton.MiddleMouse); -#if DEBUG keyMapping[(int)InputType.PreviousFireMode] = new KeyOrMouse(MouseButton.MouseWheelDown); keyMapping[(int)InputType.NextFireMode] = new KeyOrMouse(MouseButton.MouseWheelUp); -#endif + + keyMapping[(int)InputType.TakeHalfFromInventorySlot] = new KeyOrMouse(Keys.LeftShift); + keyMapping[(int)InputType.TakeOneFromInventorySlot] = new KeyOrMouse(Keys.LeftControl); if (Language == "French") { @@ -175,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)) @@ -225,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) @@ -244,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; @@ -263,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; } @@ -707,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"), @@ -727,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); @@ -745,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) @@ -811,8 +880,7 @@ namespace Barotrauma ChangeSliderText(scrollBar, scroll); SoundVolume = scroll; return true; - }, - Step = 0.05f + } }; soundScrollBar.OnMoved(soundScrollBar, soundScrollBar.BarScroll); @@ -827,8 +895,7 @@ namespace Barotrauma ChangeSliderText(scrollBar, scroll); MusicVolume = scroll; return true; - }, - Step = 0.05f + } }; musicScrollBar.OnMoved(musicScrollBar, musicScrollBar.BarScroll); @@ -837,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) => @@ -1016,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, @@ -1098,13 +1177,14 @@ namespace Barotrauma style: "GUISlider", barSize: 0.05f) { UserData = micVolumeText, - Range = new Vector2(0,540), - Step = 1.0f / 9.0f + Range = new Vector2(0, ((float)VoipConfig.BUFFER_SIZE / (float)VoipConfig.FREQUENCY) * 1000.0f * 25.0f), + Step = 1.0f / 25.0f }; cutoffPreventionSlider.BarScrollValue = VoiceChatCutoffPrevention; cutoffPreventionSlider.OnMoved = (scrollBar, scroll) => { - VoiceChatCutoffPrevention = (int)scrollBar.BarScrollValue; + int bufferMsLength = (int)(((float)VoipConfig.BUFFER_SIZE / (float)VoipConfig.FREQUENCY) * 1000.0f); + VoiceChatCutoffPrevention = (int)Math.Round(scrollBar.BarScrollValue / bufferMsLength) * bufferMsLength; cutoffPreventionText.Text = TextManager.Get("CutoffPrevention") + " " + TextManager.GetWithVariable("timeformatmilliseconds", "[milliseconds]", VoiceChatCutoffPrevention.ToString()); return true; @@ -1142,6 +1222,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; @@ -1185,7 +1266,7 @@ namespace Barotrauma AimAssistAmount = MathHelper.Lerp(0.0f, 5.0f, scroll); return true; }, - Step = 0.1f + Step = 0.01f }; aimAssistSlider.OnMoved(aimAssistSlider, aimAssistSlider.BarScroll); @@ -1201,19 +1282,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 }; @@ -1228,14 +1311,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 }; @@ -1246,11 +1332,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), @@ -1599,7 +1694,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; } @@ -1618,7 +1713,9 @@ namespace Barotrauma { DisableRegularPackage(contentPackage); } - + + ContentPackage.SortContentPackages(cp => contentPackageList.Content.GetChildIndex(contentPackageList.Content.GetChildByUserData(cp)), false, this); + UnsavedSettings = true; return true; } @@ -1709,22 +1806,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..2a6287997 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 || !IsInLimbSlot(doubleClickedItem, InvSlotType.Any)) + { + 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/Growable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Growable.cs index 27c833862..f5a2cda6e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Growable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Growable.cs @@ -86,12 +86,12 @@ namespace Barotrauma.Items.Components float scale = VineScale * vine.VineStep; - if (VineAtlas != null) + if (VineAtlas != null && VineAtlas.Loaded) { spriteBatch.Draw(VineAtlas.Texture, pos + vine.offset, vineSprite.SourceRect, color, 0f, vineSprite.AbsoluteOrigin, scale, SpriteEffects.None, layer3); } - if (DecayAtlas != null) + if (DecayAtlas != null && DecayAtlas.Loaded) { spriteBatch.Draw(DecayAtlas.Texture, pos, vineSprite.SourceRect, vine.HealthColor, 0f, vineSprite.AbsoluteOrigin, scale, SpriteEffects.None, layer2); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs index 14767d23e..a223189a9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs @@ -2,9 +2,6 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; namespace Barotrauma.Items.Components { @@ -17,7 +14,11 @@ namespace Barotrauma.Items.Components public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) { - if (!IsActive || picker == null || !CanBeAttached(picker) || !picker.IsKeyDown(InputType.Aim) || picker != Character.Controlled) { return; } + if (!IsActive || picker == null || !CanBeAttached(picker) || !picker.IsKeyDown(InputType.Aim) || picker != Character.Controlled) + { + Drawable = false; + return; + } Vector2 gridPos = picker.Position; Vector2 roundedGridPos = new Vector2( @@ -46,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 a1821a13f..c9c4fcb7a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs @@ -55,13 +55,8 @@ namespace Barotrauma.Items.Components { if (character == null || !character.IsKeyDown(InputType.Aim)) return; -#if DEBUG if (PlayerInput.KeyHit(InputType.PreviousFireMode)) -#else - if (PlayerInput.MouseWheelDownClicked()) -#endif { - if (spraySetting > 0) { spraySetting--; @@ -74,11 +69,7 @@ namespace Barotrauma.Items.Components targetSections.Clear(); } -#if DEBUG if (PlayerInput.KeyHit(InputType.NextFireMode)) -#else - if (PlayerInput.MouseWheelUpClicked()) -#endif { if (spraySetting < 2) { @@ -139,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; @@ -248,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 218269d2b..ee39accbf 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) @@ -147,6 +179,23 @@ namespace Barotrauma.Items.Components { CanBeFocused = false }; + + // Expand the frame vertically if it's too small to fit the text + if (label != null && label.RectTransform.RelativeSize.Y > 0.5f) + { + int newHeight = (int)(GuiFrame.Rect.Height + (2 * (label.RectTransform.RelativeSize.Y - 0.5f) * content.Rect.Height)); + if (newHeight > GuiFrame.RectTransform.MaxSize.Y) + { + Point newMaxSize = GuiFrame.RectTransform.MaxSize; + newMaxSize.Y = newHeight; + GuiFrame.RectTransform.MaxSize = newMaxSize; + } + GuiFrame.RectTransform.Resize(new Point(GuiFrame.Rect.Width, newHeight)); + content.RectTransform.Resize(GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin); + label.CalculateHeightFromText(); + guiCustomComponent.RectTransform.Resize(new Vector2(1.0f, Math.Max(1.0f - label.RectTransform.RelativeSize.Y, minInventoryAreaSize))); + } + Inventory.RectTransform = guiCustomComponent.RectTransform; } @@ -173,15 +222,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; @@ -257,13 +300,11 @@ namespace Barotrauma.Items.Components spriteEffects |= MathUtils.NearlyEqual(ItemRotation % 180, 90.0f) ? SpriteEffects.FlipHorizontally : SpriteEffects.FlipVertically; } - bool isWiringMode = SubEditorScreen.IsWiringMode(); + 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 +354,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/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index 047b5375a..d98beb236 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -21,18 +21,14 @@ namespace Barotrauma.Items.Components private readonly List displayedSubs = new List(); + private Point prevResolution; + partial void InitProjSpecific(XElement element) { noPowerTip = TextManager.Get("SteeringNoPowerTip"); CreateGUI(); } - protected override void OnResolutionChanged() - { - base.OnResolutionChanged(); - CreateHUD(); - } - protected override void CreateGUI() { GuiFrame.RectTransform.RelativeOffset = new Vector2(0.05f, 0.0f); @@ -76,15 +72,10 @@ namespace Barotrauma.Items.Components hullInfoFrame.AddToGUIUpdateList(order: 1); } - public override void OnMapLoaded() - { - base.OnMapLoaded(); - CreateHUD(); - } - private void CreateHUD() { - submarineContainer.ClearChildren(); + prevResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); + submarineContainer?.ClearChildren(); if (item.Submarine == null) { return; } @@ -94,19 +85,15 @@ namespace Barotrauma.Items.Components displayedSubs.AddRange(item.Submarine.DockedTo); } - public override void FlipX(bool relativeToSub) - { - CreateHUD(); - } - public override void UpdateHUD(Character character, float deltaTime, Camera cam) { //recreate HUD if the subs we should display have changed - if ((item.Submarine == null && displayedSubs.Count > 0) || //item not inside a sub anymore, but display is still showing subs - !displayedSubs.Contains(item.Submarine) || //current sub not displayer - item.Submarine.DockedTo.Any(s => !displayedSubs.Contains(s)) || //some of the docked subs not diplayed - !submarineContainer.Children.Any() || // We lack a GUI - displayedSubs.Any(s => s != item.Submarine && !item.Submarine.DockedTo.Contains(s))) //displaying a sub that shouldn't be displayed + if ((item.Submarine == null && displayedSubs.Count > 0) || //item not inside a sub anymore, but display is still showing subs + !displayedSubs.Contains(item.Submarine) || //current sub not displayer + prevResolution.X != GameMain.GraphicsWidth || prevResolution.Y != GameMain.GraphicsHeight || //resolution changed + item.Submarine.DockedTo.Any(s => !displayedSubs.Contains(s)) || //some of the docked subs not diplayed + !submarineContainer.Children.Any() || // We lack a GUI + displayedSubs.Any(s => s != item.Submarine && !item.Submarine.DockedTo.Contains(s))) //displaying a sub that shouldn't be displayed { CreateHUD(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs index 9544a0116..e35c9a17b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs @@ -215,15 +215,33 @@ namespace Barotrauma.Items.Components public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { + int msgStartPos = msg.BitPosition; + + float flowPercentage = msg.ReadRangedInteger(-10, 10) * 10.0f; + bool isActive = msg.ReadBoolean(); + bool hijacked = msg.ReadBoolean(); + float? targetLevel; + if (msg.ReadBoolean()) + { + targetLevel = msg.ReadSingle(); + } + else + { + targetLevel = null; + } + if (correctionTimer > 0.0f) { - StartDelayedCorrection(type, msg.ExtractBits(5 + 1), sendingTime); + int msgLength = msg.BitPosition - msgStartPos; + msg.BitPosition = msgStartPos; + StartDelayedCorrection(type, msg.ExtractBits(msgLength), sendingTime); return; } - FlowPercentage = msg.ReadRangedInteger(-10, 10) * 10.0f; - IsActive = msg.ReadBoolean(); - Hijacked = msg.ReadBoolean(); + FlowPercentage = flowPercentage; + IsActive = isActive; + Hijacked = hijacked; + TargetLevel = targetLevel; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs index c1bc2bc15..b8d80bbb9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs @@ -502,8 +502,16 @@ namespace Barotrauma.Items.Components { if (item.Removed) { return; } + Vector2 clampedOptimalTurbineOutput = optimalTurbineOutput; + Vector2 clampedAllowedTurbineOutput = allowedTurbineOutput; + if (clampedOptimalTurbineOutput.X > 100.0f) + { + clampedOptimalTurbineOutput = new Vector2(92.0f, 110.0f); + clampedAllowedTurbineOutput = new Vector2(85.0f, 110.0f); + } + DrawMeter(spriteBatch, container.Rect, - turbineOutputMeter, TurbineOutput, new Vector2(0.0f, 100.0f), optimalTurbineOutput, allowedTurbineOutput); + turbineOutputMeter, TurbineOutput, new Vector2(0.0f, 100.0f), clampedOptimalTurbineOutput, clampedAllowedTurbineOutput); } public override void UpdateHUD(Character character, float deltaTime, Camera cam) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index 113d693e5..8bc8e0d11 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -15,7 +15,8 @@ namespace Barotrauma.Items.Components public enum BlipType { Default, - Disruption + Disruption, + Destructible } private PathFinder pathFinder; @@ -54,7 +55,7 @@ namespace Barotrauma.Items.Components private Sprite sonarBlip; private Sprite lineSprite; - private readonly Dictionary targetIcons = new Dictionary(); + private readonly Dictionary> targetIcons = new Dictionary>(); private float displayBorderSize; @@ -70,6 +71,14 @@ namespace Barotrauma.Items.Components private float showDirectionalIndicatorTimer; + 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>(); @@ -109,6 +118,10 @@ namespace Barotrauma.Items.Components { BlipType.Disruption, new Color[] { Color.TransparentBlack, new Color(254, 68, 19), new Color(255, 220, 62), new Color(255, 255, 255) } + }, + { + BlipType.Destructible, + new Color[] { Color.TransparentBlack, new Color(74, 113, 75) * 0.8f, new Color(151, 236, 172) * 0.8f, new Color(153, 217, 234) * 0.8f } } }; @@ -124,11 +137,22 @@ namespace Barotrauma.Items.Components private readonly List textBlocksToScaleAndNormalize = new List(); + private bool isConnectedToSteering; + + private static string caveLabel; + + private bool AllowUsingMineralScanner => + HasMineralScanner && !isConnectedToSteering; + partial void InitProjSpecific(XElement element) { 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()) @@ -161,7 +185,9 @@ namespace Barotrauma.Items.Components break; case "icon": var targetIconSprite = new Sprite(subElement); - targetIcons.Add(subElement.GetAttributeString("identifier", ""), targetIconSprite); + var color = subElement.GetAttributeColor("color", Color.White); + targetIcons.Add(subElement.GetAttributeString("identifier", ""), + new Tuple(targetIconSprite, color)); break; } } @@ -176,16 +202,20 @@ namespace Barotrauma.Items.Components protected override void CreateGUI() { - bool isConnectedToSteering = item.GetComponent() != null; - Vector2 size = isConnectedToSteering ? controlBoxSize : new Vector2(controlBoxSize.X * 2.0f, controlBoxSize.Y); + isConnectedToSteering = item.GetComponent() != null; + Vector2 size = isConnectedToSteering ? controlBoxSize : new Vector2(0.46f, 0.4f); - controlContainer = new GUIFrame(new RectTransform(size, GuiFrame.RectTransform, Anchor.BottomRight, Pivot.BottomLeft), "ItemUI"); + controlContainer = new GUIFrame(new RectTransform(size, GuiFrame.RectTransform, Anchor.BottomLeft), "ItemUI"); + if (!isConnectedToSteering && !GUI.IsFourByThree()) + { + controlContainer.RectTransform.MaxSize = new Point((int)(380 * GUI.xScale), (int)(300 * GUI.yScale)); + } var paddedControlContainer = new GUIFrame(new RectTransform(controlContainer.Rect.Size - GUIStyle.ItemFrameMargin, controlContainer.RectTransform, Anchor.Center) { AbsoluteOffset = GUIStyle.ItemFrameOffset }, style: null); // Based on the height difference to the steering control box so that the elements keep the same size - float extraHeight = 0.03f; + float extraHeight = 0.0694f; var sonarModeArea = new GUIFrame(new RectTransform(new Vector2(1, 0.4f + extraHeight), paddedControlContainer.RectTransform, Anchor.TopCenter), style: null); SonarModeSwitch = new GUIButton(new RectTransform(new Vector2(0.2f, 1), sonarModeArea.RectTransform), string.Empty, style: "SwitchVertical") { @@ -224,6 +254,8 @@ namespace Barotrauma.Items.Components }; passiveTickBox.TextBlock.OverrideTextColor(GUI.Style.TextColor); activeTickBox.TextBlock.OverrideTextColor(GUI.Style.TextColor); + + textBlocksToScaleAndNormalize.Clear(); textBlocksToScaleAndNormalize.Add(passiveTickBox.TextBlock); textBlocksToScaleAndNormalize.Add(activeTickBox.TextBlock); @@ -249,7 +281,8 @@ namespace Barotrauma.Items.Components } }; - new GUIFrame(new RectTransform(new Vector2(0.8f, 0.01f), paddedControlContainer.RectTransform, Anchor.Center), style: "HorizontalLine"); + new GUIFrame(new RectTransform(new Vector2(0.8f, 0.01f), paddedControlContainer.RectTransform, Anchor.Center), style: "HorizontalLine") + { UserData = "horizontalline" }; var directionalModeFrame = new GUIFrame(new RectTransform(new Vector2(1, 0.45f), lowerAreaFrame.RectTransform, Anchor.BottomCenter), style: null); directionalModeSwitch = new GUIButton(new RectTransform(new Vector2(0.3f, 0.8f), directionalModeFrame.RectTransform, Anchor.CenterLeft), string.Empty, style: "SwitchHorizontal") @@ -270,6 +303,15 @@ namespace Barotrauma.Items.Components TextManager.Get("SonarDirectionalPing"), GUI.Style.TextColor, GUI.SubHeadingFont, Alignment.CenterLeft); textBlocksToScaleAndNormalize.Add(directionalModeSwitchText); + if (AllowUsingMineralScanner) + { + AddMineralScannerSwitchToGUI(); + } + else + { + mineralScannerSwitch = null; + } + GuiFrame.CanBeFocused = false; GUITextBlock.AutoScaleAndNormalize(textBlocksToScaleAndNormalize); @@ -284,12 +326,17 @@ namespace Barotrauma.Items.Components if (isConnectedToSteering) { controlContainer.RectTransform.RelativeOffset = controlBoxOffset; - controlContainer.RectTransform.SetPosition(Anchor.TopLeft); + controlContainer.RectTransform.SetPosition(Anchor.TopRight); sonarView.RectTransform.ScaleBasis = ScaleBasis.Smallest; - sonarView.RectTransform.SetPosition(Anchor.CenterRight); + sonarView.RectTransform.SetPosition(Anchor.CenterLeft); sonarView.RectTransform.Resize(GUISizeCalculation); GUITextBlock.AutoScaleAndNormalize(textBlocksToScaleAndNormalize); } + else if (GUI.RelativeHorizontalAspectRatio > 0.75f) + { + sonarView.RectTransform.RelativeOffset = new Vector2(0.13f * GUI.RelativeHorizontalAspectRatio, 0); + sonarView.RectTransform.SetPosition(Anchor.BottomRight); + } } private void SetPingDirection(Vector2 direction) @@ -306,20 +353,39 @@ namespace Barotrauma.Items.Components { base.OnItemLoaded(); zoomSlider.BarScroll = MathUtils.InverseLerp(MinZoom, MaxZoom, zoom); - if (HasMineralScanner) { AddMineralScannerSwitchToGUI(); } + if (AllowUsingMineralScanner && mineralScannerSwitch == null) + { + AddMineralScannerSwitchToGUI(); + GUITextBlock.AutoScaleAndNormalize(textBlocksToScaleAndNormalize); + } //make the sonarView customcomponent render the steering view so it gets drawn in front of the sonar item.GetComponent()?.AttachToSonarHUD(sonarView); } private void AddMineralScannerSwitchToGUI() { - // First adjust other elements of the lower area - zoomSlider.Parent.RectTransform.RelativeSize = new Vector2(1.0f, 0.3f); - directionalModeSwitch.Parent.RectTransform.RelativeSize = new Vector2(1.0f, 0.3f); + // First adjust other elements to make room for the additional switch + controlContainer.RectTransform.RelativeSize = new Vector2( + controlContainer.RectTransform.RelativeSize.X, + controlContainer.RectTransform.RelativeSize.Y * 1.25f); + SonarModeSwitch.Parent.RectTransform.RelativeSize = new Vector2( + SonarModeSwitch.Parent.RectTransform.RelativeSize.X, + SonarModeSwitch.Parent.RectTransform.RelativeSize.Y * 0.8f); + lowerAreaFrame.Parent.GetChildByUserData("horizontalline").RectTransform.RelativeOffset = + new Vector2(0.0f, -0.1f); + lowerAreaFrame.RectTransform.RelativeSize = new Vector2( + lowerAreaFrame.RectTransform.RelativeSize.X, + lowerAreaFrame.RectTransform.RelativeSize.Y * 1.2f); + zoomSlider.Parent.RectTransform.RelativeSize = new Vector2( + zoomSlider.Parent.RectTransform.RelativeSize.X, + zoomSlider.Parent.RectTransform.RelativeSize.Y * (2.0f / 3.0f)); + directionalModeSwitch.Parent.RectTransform.RelativeSize = new Vector2( + directionalModeSwitch.Parent.RectTransform.RelativeSize.X, + zoomSlider.Parent.RectTransform.RelativeSize.Y); directionalModeSwitch.Parent.RectTransform.SetPosition(Anchor.Center); // Then add the scanner switch - var mineralScannerFrame = new GUIFrame(new RectTransform(new Vector2(1, 0.3f), lowerAreaFrame.RectTransform, Anchor.BottomCenter), style: null); + var mineralScannerFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, zoomSlider.Parent.RectTransform.RelativeSize.Y), lowerAreaFrame.RectTransform, Anchor.BottomCenter), style: null); mineralScannerSwitch = new GUIButton(new RectTransform(new Vector2(0.3f, 0.8f), mineralScannerFrame.RectTransform, Anchor.CenterLeft), string.Empty, style: "SwitchHorizontal") { OnClicked = (button, data) => @@ -337,7 +403,6 @@ namespace Barotrauma.Items.Components var mineralScannerSwitchText = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1), mineralScannerFrame.RectTransform, Anchor.CenterRight), TextManager.Get("SonarMineralScanner"), GUI.Style.TextColor, GUI.SubHeadingFont, Alignment.CenterLeft); textBlocksToScaleAndNormalize.Add(mineralScannerSwitchText); - GUITextBlock.AutoScaleAndNormalize(textBlocksToScaleAndNormalize); } public override void UpdateHUD(Character character, float deltaTime, Camera cam) @@ -358,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; @@ -382,7 +467,7 @@ namespace Barotrauma.Items.Components Vector2.DistanceSquared(sonarView.Rect.Center.ToVector2(), PlayerInput.MousePosition) < (sonarView.Rect.Width / 2 * sonarView.Rect.Width / 2); - if (HasMineralScanner && Level.Loaded != null && !Level.Loaded.Generating) + if (AllowUsingMineralScanner && Level.Loaded != null && !Level.Loaded.Generating) { if (MineralClusters == null) { @@ -417,26 +502,42 @@ namespace Barotrauma.Items.Components if (Level.Loaded != null) { + nearbyObjectUpdateTimer -= deltaTime; + if (nearbyObjectUpdateTimer <= 0.0f) + { + nearbyObjects.Clear(); + foreach (var nearbyObject in Level.Loaded.LevelObjectManager.GetAllObjects(transducerCenter, range * zoom)) + { + if (!nearbyObject.VisibleOnSonar) { continue; } + float objectRange = range + nearbyObject.SonarRadius; + if (Vector2.DistanceSquared(transducerCenter, nearbyObject.WorldPosition) < objectRange * objectRange) + { + nearbyObjects.Add(nearbyObject); + } + } + nearbyObjectUpdateTimer = NearbyObjectUpdateInterval; + } + List ballastFloraSpores = new List(); Dictionary levelTriggerFlows = new Dictionary(); for (var pingIndex = 0; pingIndex < activePingsCount; ++pingIndex) { var activePing = activePings[pingIndex]; - LevelObjectManager objManager = Level.Loaded.LevelObjectManager; float pingRange = range * activePing.State / zoom; - foreach (LevelObject levelObject in objManager.GetAllObjects(transducerCenter, pingRange)) + foreach (LevelObject levelObject in nearbyObjects) { if (levelObject.Triggers == null) { continue; } //gather all nearby triggers that are causing the water to flow into the dictionary foreach (LevelTrigger trigger in levelObject.Triggers) { Vector2 flow = trigger.GetWaterFlowVelocity(); - //ignore ones that are barely doing anything (flow^2 < 1) - if (flow.LengthSquared() > 1.0f && !levelTriggerFlows.ContainsKey(trigger)) + //ignore ones that are barely doing anything (flow^2 <= 1) + if (flow.LengthSquared() >= 1.0f && !levelTriggerFlows.ContainsKey(trigger)) { levelTriggerFlows.Add(trigger, flow); } - if (!string.IsNullOrWhiteSpace(trigger.InfectIdentifier) && Vector2.DistanceSquared(transducerCenter, trigger.WorldPosition) < pingRange / 2 * pingRange / 2) + if (!string.IsNullOrWhiteSpace(trigger.InfectIdentifier) && + Vector2.DistanceSquared(transducerCenter, trigger.WorldPosition) < pingRange / 2 * pingRange / 2) { ballastFloraSpores.Add(trigger); } @@ -727,8 +828,8 @@ namespace Barotrauma.Items.Components float directionalPingVisibility = useDirectionalPing && currentMode == Mode.Active ? 1.0f : showDirectionalIndicatorTimer; if (directionalPingVisibility > 0.0f) { - Vector2 sector1 = MathUtils.RotatePointAroundTarget(pingDirection * DisplayRadius, Vector2.Zero, DirectionalPingSector * 0.5f); - Vector2 sector2 = MathUtils.RotatePointAroundTarget(pingDirection * DisplayRadius, Vector2.Zero, -DirectionalPingSector * 0.5f); + Vector2 sector1 = MathUtils.RotatePointAroundTarget(pingDirection * DisplayRadius, Vector2.Zero, MathHelper.ToRadians(DirectionalPingSector * 0.5f)); + Vector2 sector2 = MathUtils.RotatePointAroundTarget(pingDirection * DisplayRadius, Vector2.Zero, MathHelper.ToRadians(-DirectionalPingSector * 0.5f)); DrawLine(spriteBatch, Vector2.Zero, sector1, Color.LightCyan * 0.2f * directionalPingVisibility, width: 3); DrawLine(spriteBatch, Vector2.Zero, sector2, Color.LightCyan * 0.2f * directionalPingVisibility, width: 3); } @@ -761,7 +862,7 @@ namespace Barotrauma.Items.Components { DrawMarker(spriteBatch, Level.Loaded.StartLocation.Name, - "outpost", + Level.Loaded.StartOutpost != null ? "outpost" : "location", Level.Loaded.StartLocation.Name, Level.Loaded.StartPosition, transducerCenter, displayScale, center, DisplayRadius); @@ -771,16 +872,28 @@ namespace Barotrauma.Items.Components { DrawMarker(spriteBatch, Level.Loaded.EndLocation.Name, - "outpost", + Level.Loaded.EndOutpost != null ? "outpost" : "location", Level.Loaded.EndLocation.Name, Level.Loaded.EndPosition, transducerCenter, 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) { @@ -793,10 +906,8 @@ namespace Barotrauma.Items.Components } } - if (GameMain.GameSession.Mission != null) + foreach (Mission mission in GameMain.GameSession.Missions) { - var mission = GameMain.GameSession.Mission; - if (!string.IsNullOrWhiteSpace(mission.SonarLabel)) { foreach (Vector2 sonarPosition in mission.SonarPositions) @@ -811,18 +922,17 @@ namespace Barotrauma.Items.Components } } - if (HasMineralScanner && useMineralScanner && CurrentMode == Mode.Active && MineralClusters != null) + if (AllowUsingMineralScanner && useMineralScanner && CurrentMode == Mode.Active && MineralClusters != null) { - var maxMineralScanRangeSquared = Range * Range; foreach (var t in MineralClusters) { var unobtainedMinerals = t.Item2.Where(i => i != null && i.GetRootInventoryOwner() == i); if (unobtainedMinerals.None()) { continue; } - if (Vector2.DistanceSquared(transducerCenter, t.Item1) > maxMineralScanRangeSquared) { continue; } + if (!CheckResourceMarkerVisibility(t.Item1, transducerCenter)) { continue; } var i = unobtainedMinerals.FirstOrDefault(); if (i == null) { continue; } DrawMarker(spriteBatch, - i.Name, null, i, + i.Name, "mineral", i, t.Item1, transducerCenter, displayScale, center, DisplayRadius * 0.95f, onlyShowTextOnMouseOver: true); @@ -832,16 +942,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, @@ -861,10 +966,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); @@ -948,8 +1051,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; } @@ -1075,8 +1178,7 @@ namespace Barotrauma.Items.Components for (var pingIndex = 0; pingIndex < activePingsCount; ++pingIndex) { - var activePing = activePings[pingIndex]; - foreach (LevelObject levelObject in Level.Loaded.LevelObjectManager.GetAllObjects(pingSource, range * activePing.State)) + foreach (LevelObject levelObject in nearbyObjects) { if (levelObject.ActivePrefab?.SonarDisruption <= 0.0f) { continue; } @@ -1157,19 +1259,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++) @@ -1209,9 +1302,9 @@ namespace Barotrauma.Items.Components { foreach (Voronoi2.GraphEdge edge in cell.Edges) { - if (!edge.IsSolid) continue; + if (!edge.IsSolid) { continue; } float cellDot = Vector2.Dot(cell.Center - pingSource, (edge.Center + cell.Translation) - cell.Center); - if (cellDot > 0) continue; + if (cellDot > 0) { continue; } float facingDot = Vector2.Dot( Vector2.Normalize(edge.Point1 - edge.Point2), @@ -1222,7 +1315,8 @@ namespace Barotrauma.Items.Components edge.Point2 + cell.Translation, pingSource, transducerPos, pingRadius, prevPingRadius, - 350.0f, 3.0f * (Math.Abs(facingDot) + 1.0f), range, pingStrength, passive); + 350.0f, 3.0f * (Math.Abs(facingDot) + 1.0f), range, pingStrength, passive, + blipType : cell.IsDestructible ? BlipType.Destructible : BlipType.Default); } } @@ -1311,7 +1405,7 @@ namespace Barotrauma.Items.Components } private void CreateBlipsForLine(Vector2 point1, Vector2 point2, Vector2 pingSource, Vector2 transducerPos, float pingRadius, float prevPingRadius, - float lineStep, float zStep, float range, float pingStrength, bool passive) + float lineStep, float zStep, float range, float pingStrength, bool passive, BlipType blipType = BlipType.Default) { lineStep /= zoom; zStep /= zoom; @@ -1327,13 +1421,13 @@ namespace Barotrauma.Items.Components //ignore if outside the display Vector2 transducerDiff = point - transducerPos; Vector2 transducerDisplayDiff = transducerDiff * displayScale; - if (transducerDisplayDiff.LengthSquared() > DisplayRadius * DisplayRadius) continue; + if (transducerDisplayDiff.LengthSquared() > DisplayRadius * DisplayRadius) { continue; } //ignore if the point is not within the ping Vector2 pointDiff = point - pingSource; Vector2 displayPointDiff = pointDiff * displayScale; float displayPointDistSqr = displayPointDiff.LengthSquared(); - if (displayPointDistSqr < prevPingRadius * prevPingRadius || displayPointDistSqr > pingRadius * pingRadius) continue; + if (displayPointDistSqr < prevPingRadius * prevPingRadius || displayPointDistSqr > pingRadius * pingRadius) { continue; } //ignore if direction is disrupted float transducerDist = transducerDiff.Length(); @@ -1348,7 +1442,7 @@ namespace Barotrauma.Items.Components break; } } - if (disrupted) continue; + if (disrupted) { continue; } float displayPointDist = (float)Math.Sqrt(displayPointDistSqr); float alpha = pingStrength * Rand.Range(1.5f, 2.0f); @@ -1360,8 +1454,8 @@ namespace Barotrauma.Items.Components int minDist = (int)(200 / zoom); sonarBlips.RemoveAll(b => b.FadeTimer < fadeTimer && Math.Abs(pos.X - b.Position.X) < minDist && Math.Abs(pos.Y - b.Position.Y) < minDist); - var blip = new SonarBlip(pos, fadeTimer, 1.0f + ((displayPointDist + z) / DisplayRadius)); - if (!passive && !CheckBlipVisibility(blip, transducerPos)) continue; + var blip = new SonarBlip(pos, fadeTimer, 1.0f + ((displayPointDist + z) / DisplayRadius), blipType); + if (!passive && !CheckBlipVisibility(blip, transducerPos)) { continue; } sonarBlips.Add(blip); zStep += 0.5f / zoom; @@ -1375,7 +1469,7 @@ namespace Barotrauma.Items.Components alpha -= 0.1f; } - if (alpha < 0) break; + if (alpha < 0) { break; } } } } @@ -1404,6 +1498,30 @@ namespace Barotrauma.Items.Components return true; } + /// + /// Based largely on existing CheckBlipVisibility() code + /// + private bool CheckResourceMarkerVisibility(Vector2 resourcePos, Vector2 transducerPos) + { + var distSquared = Vector2.DistanceSquared(transducerPos, resourcePos); + if (distSquared > Range * Range) + { + return false; + } + if (currentPingIndex != -1 && activePings[currentPingIndex].IsDirectional) + { + var pos = (resourcePos - transducerPos) * displayScale * zoom; + pos.Y = -pos.Y; + var length = pos.Length(); + var dir = pos / length; + if (Vector2.Dot(activePings[currentPingIndex].Direction, dir) < DirectionalPingDotProduct) + { + return false; + } + } + return true; + } + private void DrawBlip(SpriteBatch spriteBatch, SonarBlip blip, Vector2 transducerPos, Vector2 center, float strength, float blipScale) { strength = MathHelper.Clamp(strength, 0.0f, 1.0f); @@ -1524,13 +1642,14 @@ namespace Barotrauma.Items.Components } } - if (string.IsNullOrEmpty(iconIdentifier) || !targetIcons.ContainsKey(iconIdentifier)) + if (iconIdentifier == null || !targetIcons.ContainsKey(iconIdentifier)) { GUI.DrawRectangle(spriteBatch, new Rectangle((int)markerPos.X - 3, (int)markerPos.Y - 3, 6, 6), markerColor, thickness: 2); } else { - targetIcons[iconIdentifier].Draw(spriteBatch, markerPos); + var iconInfo = targetIcons[iconIdentifier]; + iconInfo.Item1.Draw(spriteBatch, markerPos, iconInfo.Item2); } if (alpha <= 0.0f) { return; } @@ -1561,9 +1680,9 @@ namespace Barotrauma.Items.Components screenBackground?.Remove(); lineSprite?.Remove(); - foreach (Sprite sprite in targetIcons.Values) + foreach (var t in targetIcons.Values) { - sprite.Remove(); + t.Item1.Remove(); } targetIcons.Clear(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs index fb5036c77..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; @@ -112,7 +112,7 @@ namespace Barotrauma.Items.Components protected override void CreateGUI() { - controlContainer = new GUIFrame(new RectTransform(new Vector2(Sonar.controlBoxSize.X, 1 - Sonar.controlBoxSize.Y * 2), GuiFrame.RectTransform, Anchor.CenterLeft), "ItemUI"); + controlContainer = new GUIFrame(new RectTransform(new Vector2(Sonar.controlBoxSize.X, 1 - Sonar.controlBoxSize.Y * 2), GuiFrame.RectTransform, Anchor.CenterRight), "ItemUI"); var paddedControlContainer = new GUIFrame(new RectTransform(controlContainer.Rect.Size - GUIStyle.ItemFrameMargin, controlContainer.RectTransform, Anchor.Center) { AbsoluteOffset = GUIStyle.ItemFrameOffset @@ -265,7 +265,7 @@ namespace Barotrauma.Items.Components levelStartSelected ? Destination.LevelStart : Destination.LevelEnd); // Status -> - statusContainer = new GUIFrame(new RectTransform(Sonar.controlBoxSize, GuiFrame.RectTransform, Anchor.BottomLeft) + statusContainer = new GUIFrame(new RectTransform(Sonar.controlBoxSize, GuiFrame.RectTransform, Anchor.BottomRight) { RelativeOffset = Sonar.controlBoxOffset }, "ItemUI"); @@ -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; @@ -339,9 +340,9 @@ namespace Barotrauma.Items.Components //docking interface ---------------------------------------------------- float dockingButtonSize = 1.1f; float elementScale = 0.6f; - dockingContainer = new GUIFrame(new RectTransform(Sonar.controlBoxSize, GuiFrame.RectTransform, Anchor.BottomLeft, scaleBasis: ScaleBasis.Smallest) + dockingContainer = new GUIFrame(new RectTransform(Sonar.controlBoxSize, GuiFrame.RectTransform, Anchor.BottomRight, scaleBasis: ScaleBasis.Smallest) { - RelativeOffset = new Vector2(Sonar.controlBoxOffset.X + 0.05f, Sonar.controlBoxOffset.Y) + RelativeOffset = new Vector2(Sonar.controlBoxOffset.X + 0.05f, -0.05f) }, style: null); dockText = TextManager.Get("label.navterminaldock", fallBackTag: "captain.dock"); @@ -436,12 +437,17 @@ namespace Barotrauma.Items.Components }; // Sonar area - steerArea = new GUICustomComponent(new RectTransform(Sonar.GUISizeCalculation, GuiFrame.RectTransform, Anchor.CenterRight, scaleBasis: ScaleBasis.Smallest), + steerArea = new GUICustomComponent(new RectTransform(Sonar.GUISizeCalculation, GuiFrame.RectTransform, Anchor.CenterLeft, scaleBasis: ScaleBasis.Smallest), (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 e34555c26..a57f4abfa 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RepairTool.cs @@ -66,7 +66,11 @@ namespace Barotrauma.Items.Components { foreach (ParticleEmitter particleEmitter in particleEmitters) { - float particleAngle = item.body.Rotation + MathHelper.ToRadians(BarrelRotation) + ((item.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi); + float particleAngle = MathHelper.ToRadians(BarrelRotation); + if (item.body != null) + { + particleAngle += item.body.Rotation + ((item.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi); + } particleEmitter.Emit( deltaTime, ConvertUnits.ToDisplayUnits(raystart), item.CurrentHull, particleAngle, particleEmitter.Prefab.CopyEntityAngle ? -particleAngle : 0); @@ -109,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 03488b972..18d7f5650 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs @@ -48,18 +48,14 @@ namespace Barotrauma.Items.Components Wire equippedWire = null; - bool allowRewiring = GameMain.NetworkMember?.ServerSettings == null || GameMain.NetworkMember.ServerSettings.AllowRewiring; + bool allowRewiring = GameMain.NetworkMember?.ServerSettings == null || GameMain.NetworkMember.ServerSettings.AllowRewiring || panel.AlwaysAllowRewiring; if (allowRewiring && (!panel.Locked || Screen.Selected == GameMain.SubEditorScreen)) { //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; } @@ -380,7 +381,7 @@ namespace Barotrauma.Items.Components { ConnectionPanel.HighlightedWire = wire; - bool allowRewiring = GameMain.NetworkMember?.ServerSettings == null || GameMain.NetworkMember.ServerSettings.AllowRewiring; + bool allowRewiring = GameMain.NetworkMember?.ServerSettings == null || GameMain.NetworkMember.ServerSettings.AllowRewiring || panel.AlwaysAllowRewiring; if (allowRewiring && (!wire.Locked && !panel.Locked || Screen.Selected == GameMain.SubEditorScreen)) { //start dragging the wire 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 6e57ae7c3..353e689fe 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs @@ -117,9 +117,9 @@ namespace Barotrauma.Items.Components { if (defaultWireSprite == null) { - defaultWireSprite = new Sprite("Content/Items/wireHorizontal.png", new Vector2(0.5f, 0.5f)) + defaultWireSprite = new Sprite("Content/Items/Electricity/signalcomp.png", new Rectangle(970, 47, 14, 16), new Vector2(0.5f, 0.5f)) { - Depth = 0.85f + Depth = 0.855f }; } @@ -156,7 +156,7 @@ namespace Barotrauma.Items.Components drawOffset = sub.DrawPosition + sub.HiddenSubPosition; } - float depth = item.IsSelected ? 0.0f : SubEditorScreen.IsWiringMode() ? 0.02f : wireSprite.Depth + ((item.ID % 100) * 0.00001f); + float depth = item.IsSelected ? 0.0f : SubEditorScreen.IsWiringMode() ? 0.02f : wireSprite.Depth + (item.ID % 100) * 0.000001f;// item.GetDrawDepth(wireSprite.Depth, wireSprite); if (item.IsHighlighted) { @@ -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..67772a0a2 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)); @@ -210,7 +225,7 @@ namespace Barotrauma.Items.Components foreach (AfflictionPrefab affliction in combinedAfflictionStrengths.Keys) { - texts.Add(TextManager.AddPunctuation(':', affliction.Name, ((int)combinedAfflictionStrengths[affliction]).ToString() + " %")); + texts.Add(TextManager.AddPunctuation(':', affliction.Name, Math.Max(((int)combinedAfflictionStrengths[affliction]), 1).ToString() + " %")); textColors.Add(Color.Lerp(GUI.Style.Orange, GUI.Style.Red, combinedAfflictionStrengths[affliction] / affliction.MaxStrength)); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs index bcb79e660..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) @@ -247,7 +247,7 @@ namespace Barotrauma.Items.Components public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) { - if (!MathUtils.NearlyEqual(item.Rotation, prevBaseRotation)) + if (!MathUtils.NearlyEqual(item.Rotation, prevBaseRotation) || !MathUtils.NearlyEqual(item.Scale, prevScale)) { UpdateTransformedBarrelPos(); } @@ -290,9 +290,10 @@ namespace Barotrauma.Items.Components 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); const float coneRadius = 300.0f; @@ -300,7 +301,11 @@ namespace Barotrauma.Items.Components 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); } @@ -309,12 +314,15 @@ namespace Barotrauma.Items.Components spriteBatch.DrawSector(drawPos, circleRadius, radians, (int)Math.Abs(90 * radians), GUI.Style.Green, offset: minRotation, thickness: lineThickness); } - Widget minRotationWidget = GetWidget("minrotation", spriteBatch, size: 10, initMethod: (widget) => + int baseWidgetScale = GUI.IntScale(16); + int widgetSize = (int) (Math.Max(baseWidgetScale, baseWidgetScale / Screen.Selected.Cam.Zoom)); + float widgetThickness = Math.Max(1f, lineThickness); + Widget minRotationWidget = GetWidget("minrotation", spriteBatch, size: widgetSize, thickness: widgetThickness, initMethod: (widget) => { widget.Selected += () => - { - oldRotation = RotationLimits; - }; + { + oldRotation = RotationLimits; + }; widget.MouseDown += () => { widget.color = GUI.Style.Green; @@ -324,6 +332,7 @@ namespace Barotrauma.Items.Components { widget.color = Color.Yellow; item.CreateEditingHUD(); + RotationLimits = RotationLimits; if (SubEditorScreen.IsSubEditor()) { SubEditorScreen.StoreCommand(new PropertyCommand(this, "RotationLimits", RotationLimits, oldRotation)); @@ -332,13 +341,7 @@ namespace Barotrauma.Items.Components widget.MouseHeld += (deltaTime) => { minRotation = GetRotationAngle(GetDrawPos()); - if (minRotation > maxRotation) - { - float temp = minRotation; - minRotation = maxRotation; - maxRotation = temp; - } - RotationLimits = RotationLimits; + UpdateBarrel(); MapEntity.DisableSelect = true; }; widget.PreUpdate += (deltaTime) => @@ -359,7 +362,7 @@ namespace Barotrauma.Items.Components }; }); - Widget maxRotationWidget = GetWidget("maxrotation", spriteBatch, size: 10, initMethod: (widget) => + Widget maxRotationWidget = GetWidget("maxrotation", spriteBatch, size: widgetSize, thickness: widgetThickness, initMethod: (widget) => { widget.Selected += () => { @@ -368,12 +371,13 @@ namespace Barotrauma.Items.Components widget.MouseDown += () => { widget.color = GUI.Style.Green; - prevAngle = minRotation; + prevAngle = maxRotation; }; widget.Deselected += () => { widget.color = Color.Yellow; item.CreateEditingHUD(); + RotationLimits = RotationLimits; if (SubEditorScreen.IsSubEditor()) { SubEditorScreen.StoreCommand(new PropertyCommand(this, "RotationLimits", RotationLimits, oldRotation)); @@ -382,13 +386,7 @@ namespace Barotrauma.Items.Components widget.MouseHeld += (deltaTime) => { maxRotation = GetRotationAngle(GetDrawPos()); - if (minRotation > maxRotation) - { - float temp = minRotation; - minRotation = maxRotation; - maxRotation = temp; - } - RotationLimits = RotationLimits; + UpdateBarrel(); MapEntity.DisableSelect = true; }; widget.PreUpdate += (deltaTime) => @@ -418,22 +416,32 @@ namespace Barotrauma.Items.Components drawPos.Y = -drawPos.Y; return drawPos; } + + void UpdateBarrel() + { + rotation = (minRotation + maxRotation) / 2; + } } - private Widget GetWidget(string id, SpriteBatch spriteBatch, int size = 5, Action initMethod = null) + private Widget GetWidget(string id, SpriteBatch spriteBatch, int size = 5, float thickness = 1f, Action initMethod = null) { + Vector2 offset = new Vector2(size / 2 + 5, -10); if (!widgets.TryGetValue(id, out Widget widget)) { widget = new Widget(id, size, Widget.Shape.Rectangle) { color = Color.Yellow, - tooltipOffset = new Vector2(size / 2 + 5, -10), + tooltipOffset = offset, inputAreaMargin = 20, RequireMouseOn = false }; widgets.Add(id, widget); initMethod?.Invoke(widget); } + + widget.size = size; + widget.tooltipOffset = offset; + widget.thickness = thickness; return widget; } @@ -488,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 = @@ -536,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/DockingPort.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs index 02b174c32..902e7d3ac 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs @@ -165,7 +165,14 @@ namespace Barotrauma.Items.Components if (isLocked) { - Lock(isNetworkMessage: true, forcePosition: true); + if (DockingTarget.joint != null) + { + DockingTarget.Lock(isNetworkMessage: true, forcePosition: true); + } + else + { + Lock(isNetworkMessage: true, forcePosition: true); + } } } else diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index 13dd24e00..8df0dd445 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,132 @@ 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 && + !(Character.Controlled.SelectedConstruction?.linkedTo.Contains(selectedSlot.ParentInventory?.Owner) ?? false) && + rootOwner != Character.Controlled && + rootOwner != Character.Controlled.SelectedCharacter && + rootOwner != Character.Controlled.SelectedConstruction && + !(Character.Controlled.SelectedConstruction?.linkedTo.Contains(rootOwner) ?? false)) + { + selectedSlot = null; + } + } } } @@ -1148,9 +1269,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 +1307,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 +1334,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 +1347,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 +1368,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 +1380,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 +1416,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 +1442,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 +1471,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 +1490,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 +1551,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 && inventory != null) + { + 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 +1585,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 +1723,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 dceab415f..383a3cd48 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(); @@ -85,7 +87,7 @@ namespace Barotrauma { get { - if (!GameMain.SubEditorScreen.ShowThalamus && prefab.Category.HasFlag(MapEntityCategory.Thalamus)) + if (GameMain.SubEditorScreen.IsSubcategoryHidden(prefab.Subcategory)) { return false; } @@ -93,18 +95,20 @@ namespace Barotrauma } } + public float GetDrawDepth() + { + return GetDrawDepth(SpriteDepth, Sprite); + } + public Color GetSpriteColor() { 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; @@ -115,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; @@ -130,6 +131,7 @@ namespace Barotrauma partial void SetActiveSpriteProjSpecific() { activeSprite = prefab.sprite; + activeContainedSprite = null; Holdable holdable = GetComponent(); if (holdable != null && holdable.Attached) { @@ -137,7 +139,9 @@ namespace Barotrauma { if (containedSprite.UseWhenAttached) { - activeSprite = containedSprite.Sprite; + activeContainedSprite = containedSprite; + activeSprite = containedSprite.Sprite; + UpdateSpriteStates(0.0f); return; } } @@ -149,7 +153,9 @@ namespace Barotrauma { if (containedSprite.MatchesContainer(Container)) { + activeContainedSprite = containedSprite; activeSprite = containedSprite.Sprite; + UpdateSpriteStates(0.0f); return; } } @@ -182,6 +188,7 @@ namespace Barotrauma decorativeSprite.Sprite.EnsureLazyLoaded(); spriteAnimState.Add(decorativeSprite, new DecorativeSprite.State()); } + SetActiveSprite(); UpdateSpriteStates(0.0f); } @@ -235,12 +242,20 @@ 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); - bool isWiringMode = editing && SubEditorScreen.IsWiringMode() && !isWire && parentInventory == null; + bool isWiringMode = editing && SubEditorScreen.TransparentWiringMode && SubEditorScreen.IsWiringMode() && !isWire && parentInventory == null; bool renderTransparent = isWiringMode && GetComponent() == null; if (renderTransparent) { color *= 0.15f; } @@ -300,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, @@ -323,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); @@ -331,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)); } } @@ -348,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) @@ -360,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) @@ -379,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); @@ -388,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)); } } @@ -400,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)); } @@ -500,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]; @@ -1030,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; } @@ -1254,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; @@ -1262,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 || @@ -1485,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; } @@ -1516,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 c1786c854..540d896d0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs @@ -5,6 +5,8 @@ using System.Linq; using System.Xml.Linq; using Barotrauma.Items.Components; using Barotrauma.Networking; +using Barotrauma.Particles; +using Barotrauma.Sounds; using FarseerPhysics; using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; @@ -33,12 +35,19 @@ namespace Barotrauma.MapCreatures.Behavior [Serialize(defaultValue: 0f, isSaveable: false)] public float MaxVelocity { get; set; } + [Serialize(defaultValue: "255,255,255,255", isSaveable: false)] + public Color ColorMultiplier { get; set; } + private float RandRotation() => Rand.Range(MinRotation, MaxRotation); private float RandVelocity() => Rand.Range(MinVelocity, MaxVelocity); public void Emit(Vector2 pos) { - GameMain.ParticleManager.CreateParticle(Identifier, pos, RandRotation(), RandVelocity()); + Particle particle = GameMain.ParticleManager.CreateParticle(Identifier, pos, RandRotation(), RandVelocity()); + if (particle != null) + { + particle.ColorMultiplier = ColorMultiplier.ToVector4(); + } } public DamageParticle(XElement element) @@ -54,6 +63,9 @@ namespace Barotrauma.MapCreatures.Behavior public readonly List LeafSprites = new List(), DamagedLeafSprites = new List(); public readonly List DamageParticles = new List(); + public readonly List DeathParticles = new List(); + + public static bool AlwaysShowBallastFloraSprite = false; partial void LoadPrefab(XElement element) { @@ -97,6 +109,9 @@ namespace Barotrauma.MapCreatures.Behavior case "damageparticle": DamageParticles.Add(new DamageParticle(subElement)); break; + case "deathparticle": + DeathParticles.Add(new DamageParticle(subElement)); + break; case "targets": LoadTargets(subElement); break; @@ -112,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)); } } @@ -129,11 +144,22 @@ namespace Barotrauma.MapCreatures.Behavior } } - private static readonly Color DarkColor = new Color(25, 25, 25); + private void CreateDeathParticle(BallastFloraBranch branch) + { + Vector2 pos = GetWorldPosition() + branch.Position; + int amount = (int)Math.Clamp(branch.MaxHealth / 10f, 1, 10); + for (int i = 0; i < amount; i++) + { + foreach (DamageParticle particle in DeathParticles) + { + particle.Emit(pos); + } + } + } public void Draw(SpriteBatch spriteBatch) { - const float zStep = 0.00001f; + const float zStep = 0.000001f; float leafDepth = zStep; float flowerDepth = zStep; @@ -217,12 +243,12 @@ namespace Barotrauma.MapCreatures.Behavior if (HasBrokenThrough) { - if (branchAtlas != null) + if (branchAtlas != null && branchAtlas.Loaded) { spriteBatch.Draw(branchAtlas.Texture, pos + branch.offset, branchSprite.SourceRect, branchColor, 0f, branchSprite.AbsoluteOrigin, BaseBranchScale * branch.VineStep, SpriteEffects.None, layer2); } - if (decayAtlas != null && isDamaged) + if (decayAtlas != null && isDamaged && decayAtlas.Loaded) { spriteBatch.Draw(decayAtlas.Texture, pos + branch.offset, branchSprite.SourceRect, branch.HealthColor, 0f, branchSprite.AbsoluteOrigin, BaseBranchScale * branch.VineStep, SpriteEffects.None, layer2 - zStep); } @@ -242,6 +268,10 @@ namespace Barotrauma.MapCreatures.Behavior DamagedFlowerSprites[variant].Draw(spriteBatch, pos, branch.HealthColor, flowerSprite.Origin, scale: flowerScale, rotate: branch.FlowerConfig.Rotation, depth: layer1 - flowerDepth - zStep); } flowerDepth -= zStep; + if (flowerDepth > 0.01f) + { + flowerDepth = zStep; + } } if (branch.LeafConfig.Variant >= 0 && HasBrokenThrough) @@ -254,6 +284,10 @@ namespace Barotrauma.MapCreatures.Behavior DamagedLeafSprites[variant].Draw(spriteBatch, pos, branch.HealthColor, leafSprite.Origin, scale: BaseLeafScale * branch.LeafConfig.Scale * branch.FlowerStep, rotate: branch.LeafConfig.Rotation, depth: layer3 + leafDepth - zStep); } leafDepth += zStep; + if (leafDepth > 0.01f) + { + flowerDepth = zStep; + } } } } @@ -264,25 +298,42 @@ namespace Barotrauma.MapCreatures.Behavior switch (header) { case NetworkHeader.Infect: + int infectBranch = -1; ushort itemId = msg.ReadUInt16(); bool infect = msg.ReadBoolean(); - if (Entity.FindEntityByID(itemId) is Item item) + if (infect) + { + infectBranch = msg.ReadInt32(); + } + + Entity? entity = Entity.FindEntityByID(itemId); + if (entity is Item item) { if (infect) { - ClaimTarget(item, null); + ClaimTarget(item, Branches.FirstOrDefault(b => b.ID == infectBranch)); } else { RemoveClaim(itemId); } } + else + { + DebugConsole.AddWarning($"Received Infect.{infect} Network Header with invalid item ID: {itemId}, which belongs to {entity?.ToString() ?? "null!"}"); + } break; case NetworkHeader.BranchCreate: int parentId = msg.ReadInt32(); BallastFloraBranch branch = ReadBranch(msg); + BallastFloraBranch? parent = Branches.FirstOrDefault(b => b.ID == parentId); - UpdateConnections(branch, Branches.FirstOrDefault(b => b.ID == parentId)); + if (parent == null) + { + DebugConsole.AddWarning($"Received BranchCreate with an invalid parent ID: {parentId}, Maximum ID is {Branches.Max(b => b.ID)}"); + } + + UpdateConnections(branch, parent); Branches.Add(branch); OnBranchGrowthSuccess(branch); break; @@ -290,7 +341,15 @@ namespace Barotrauma.MapCreatures.Behavior int removedBranchId = msg.ReadInt32(); BallastFloraBranch removedBranch = Branches.FirstOrDefault(b => b.ID == removedBranchId); - if (removedBranch != null) { RemoveBranch(removedBranch); } + if (removedBranch != null) + { + RemoveBranch(removedBranch); + } + else + { + DebugConsole.AddWarning($"Received BranchRemove for a branch that doesn't exist. ID: {removedBranchId}, Maximum ID is {Branches.Max(b => b.ID)}"); + } + break; case NetworkHeader.BranchDamage: @@ -303,6 +362,10 @@ namespace Barotrauma.MapCreatures.Behavior CreateDamageParticle(damagedBranch, damage); damagedBranch.Health = health; } + else + { + DebugConsole.AddWarning($"Received BranchDamage for a branch that doesn't exist. ID: {damageBranchId}, Maximum ID is {Branches.Max(b => b.ID)}"); + } break; case NetworkHeader.Kill: Kill(); @@ -326,6 +389,7 @@ namespace Barotrauma.MapCreatures.Behavior return new BallastFloraBranch(this, pos, (VineTileType)type, FoliageConfig.Deserialize(flowerConfig), FoliageConfig.Deserialize(leafConfig)) { ID = id, + MaxHealth = maxHealth, Sides = (TileSide) sides }; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Explosion.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Explosion.cs index 16da4be6a..e899eb482 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Explosion.cs @@ -22,47 +22,47 @@ namespace Barotrauma var underwaterExplosion = GameMain.ParticleManager.CreateParticle("underwaterexplosion", worldPosition, Vector2.Zero, 0.0f, hull); if (underwaterExplosion != null) { - underwaterExplosion.Size *= MathHelper.Clamp(attack.Range / 150.0f, 0.5f, 10.0f); + underwaterExplosion.Size *= MathHelper.Clamp(Attack.Range / 150.0f, 0.5f, 10.0f); underwaterExplosion.StartDelay = 0.0f; } } - for (int i = 0; i < attack.Range * 0.1f; i++) + for (int i = 0; i < Attack.Range * 0.1f; i++) { if (!underwater) { float particleSpeed = Rand.Range(0.0f, 1.0f); - particleSpeed = particleSpeed * particleSpeed * attack.Range; + particleSpeed = particleSpeed * particleSpeed * Attack.Range; if (flames) { - float particleScale = MathHelper.Clamp(attack.Range * 0.0025f, 0.5f, 2.0f); + float particleScale = MathHelper.Clamp(Attack.Range * 0.0025f, 0.5f, 2.0f); var flameParticle = GameMain.ParticleManager.CreateParticle("explosionfire", - ClampParticlePos(worldPosition + Rand.Vector((float)System.Math.Sqrt(Rand.Range(0.0f, attack.Range))), hull), + ClampParticlePos(worldPosition + Rand.Vector((float)System.Math.Sqrt(Rand.Range(0.0f, Attack.Range))), hull), Rand.Vector(Rand.Range(0.0f, particleSpeed)), 0.0f, hull); if (flameParticle != null) flameParticle.Size *= particleScale; } if (smoke) { GameMain.ParticleManager.CreateParticle(Rand.Range(0.0f, 1.0f) < 0.5f ? "explosionsmoke" : "smoke", - ClampParticlePos(worldPosition + Rand.Vector((float)System.Math.Sqrt(Rand.Range(0.0f, attack.Range))), hull), + ClampParticlePos(worldPosition + Rand.Vector((float)System.Math.Sqrt(Rand.Range(0.0f, Attack.Range))), hull), Rand.Vector(Rand.Range(0.0f, particleSpeed)), 0.0f, hull); } } else if (underwaterBubble) { - Vector2 bubblePos = Rand.Vector(Rand.Range(0.0f, attack.Range * 0.5f)); + Vector2 bubblePos = Rand.Vector(Rand.Range(0.0f, Attack.Range * 0.5f)); GameMain.ParticleManager.CreateParticle("risingbubbles", worldPosition + bubblePos, Vector2.Zero, 0.0f, hull); - if (i < attack.Range * 0.02f) + if (i < Attack.Range * 0.02f) { var underwaterExplosion = GameMain.ParticleManager.CreateParticle("underwaterexplosion", worldPosition + bubblePos, Vector2.Zero, 0.0f, hull); if (underwaterExplosion != null) { - underwaterExplosion.Size *= MathHelper.Clamp(attack.Range / 300.0f, 0.5f, 2.0f) * Rand.Range(0.8f, 1.2f); + underwaterExplosion.Size *= MathHelper.Clamp(Attack.Range / 300.0f, 0.5f, 2.0f) * Rand.Range(0.8f, 1.2f); } } @@ -77,7 +77,7 @@ namespace Barotrauma if (flash) { - float displayRange = flashRange.HasValue ? flashRange.Value : attack.Range; + float displayRange = flashRange.HasValue ? flashRange.Value : Attack.Range; if (displayRange < 0.1f) { return; } var light = new LightSource(worldPosition, displayRange, Color.LightYellow, null); CoroutineManager.StartCoroutine(DimLight(light)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs index 3663d2bb5..82627664c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs @@ -336,6 +336,7 @@ namespace Barotrauma public void DrawSectionColors(SpriteBatch spriteBatch) { + if (BackgroundSections == null || BackgroundSections.Count == 0) { return; } Vector2 drawOffset = Submarine == null ? Vector2.Zero : Submarine.DrawPosition; Point sectionSize = BackgroundSections[0].Rect.Size; Vector2 drawPos = drawOffset + new Vector2(rect.Location.X + sectionSize.X / 2, rect.Location.Y - sectionSize.Y / 2); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/CaveGenerator.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/CaveGenerator.cs index 923c7a848..7fa3a7021 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/CaveGenerator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/CaveGenerator.cs @@ -31,11 +31,20 @@ namespace Barotrauma List vertices = new List(); foreach (VoronoiCell cell in cells) { + Vector2 minVert = cell.Edges[0].Point1; + Vector2 maxVert = cell.Edges[0].Point1; float circumference = 0.0f; foreach (GraphEdge edge in cell.Edges) { circumference += Vector2.Distance(edge.Point1, edge.Point2); + minVert = new Vector2( + Math.Min(minVert.X, edge.Point1.X), + Math.Min(minVert.Y, edge.Point1.Y)); + maxVert = new Vector2( + Math.Max(maxVert.X, edge.Point1.X), + Math.Max(maxVert.Y, edge.Point1.Y)); } + Vector2 center = (minVert + maxVert) / 2; foreach (GraphEdge edge in cell.Edges) { if (!edge.IsSolid) { continue; } @@ -130,8 +139,8 @@ namespace Barotrauma break; } - float point1UV = MathUtils.WrapAngleTwoPi(MathUtils.VectorToAngle(edge.Point1 - cell.Center)); - float point2UV = MathUtils.WrapAngleTwoPi(MathUtils.VectorToAngle(edge.Point2 - cell.Center)); + float point1UV = MathUtils.WrapAngleTwoPi(MathUtils.VectorToAngle(edge.Point1 - center)); + float point2UV = MathUtils.WrapAngleTwoPi(MathUtils.VectorToAngle(edge.Point2 - center)); //handle wrapping around 0/360 if (point1UV - point2UV > MathHelper.Pi) { 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 cfa02438c..41525b40c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs @@ -3,9 +3,11 @@ using Barotrauma.Networking; using Barotrauma.Particles; using Barotrauma.Sounds; using Barotrauma.SpriteDeformations; +using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Linq; using System.Xml.Linq; namespace Barotrauma @@ -72,6 +74,18 @@ namespace Barotrauma private set; } + public bool VisibleOnSonar + { + get; + private set; + } + + public float SonarRadius + { + get; + private set; + } + partial void InitProjSpecific() { Sprite?.EnsureLazyLoaded(); @@ -135,6 +149,13 @@ namespace Barotrauma } } } + + VisibleOnSonar = Prefab.SonarDisruption > 0.0f || Prefab.OverrideProperties.Any(p => p != null && p.SonarDisruption > 0.0f) || + (Triggers != null && Triggers.Any(t => !MathUtils.NearlyEqual(t.Force, Vector2.Zero) && t.ForceMode != LevelTrigger.TriggerForceMode.LimitVelocity || !string.IsNullOrWhiteSpace(t.InfectIdentifier))); + if (VisibleOnSonar && Triggers.Any()) + { + SonarRadius = Triggers.Select(t => t.ColliderRadius * 1.5f).Max(); + } } public void Update(float deltaTime) @@ -220,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 bd027c920..ebf26774d 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) { @@ -294,7 +302,7 @@ namespace Barotrauma { GUI.DrawLine(spriteBatch, new Vector2(edge.Point1.X + cell.Translation.X, -(edge.Point1.Y + cell.Translation.Y)), new Vector2(edge.Point2.X + cell.Translation.X, -(edge.Point2.Y + cell.Translation.Y)), edge.NextToCave ? Color.Red : (cell.Body == null ? Color.Cyan * 0.5f : (edge.IsSolid ? Color.White : Color.Gray)), - width: edge.NextToCave ? 8 :1); + width: edge.NextToCave ? 8 : 1); } foreach (Vector2 point in cell.BodyVertices) @@ -314,6 +322,11 @@ namespace Barotrauma } }*/ + foreach (var abyssIsland in level.AbyssIslands) + { + GUI.DrawRectangle(spriteBatch, new Vector2(abyssIsland.Area.X, -abyssIsland.Area.Y - abyssIsland.Area.Height), abyssIsland.Area.Size.ToVector2(), Color.Cyan, thickness: 5); + } + foreach (var ruin in level.Ruins) { ruin.DebugDraw(spriteBatch); @@ -395,20 +408,20 @@ namespace Barotrauma graphicsDevice.SetVertexBuffer(wall.WallBuffer); graphicsDevice.DrawPrimitives(PrimitiveType.TriangleList, 0, (int)Math.Floor(wall.WallBuffer.VertexCount / 3.0f)); + if (destructibleWall.Damage > 0.0f) + { + wallCenterEffect.Texture = level.GenerationParams.WallSpriteDestroyed.Texture; + wallCenterEffect.Alpha = MathHelper.Lerp(0.2f, 1.0f, destructibleWall.Damage / destructibleWall.MaxHealth) * wall.Alpha; + wallCenterEffect.CurrentTechnique.Passes[0].Apply(); + graphicsDevice.DrawPrimitives(PrimitiveType.TriangleList, 0, (int)Math.Floor(wall.WallEdgeBuffer.VertexCount / 3.0f)); + } + wallEdgeEffect.Texture = level.GenerationParams.DestructibleWallEdgeSprite?.Texture ?? level.GenerationParams.WallEdgeSprite.Texture; wallEdgeEffect.World = wall.GetTransform() * transformMatrix; wallEdgeEffect.Alpha = wall.Alpha; wallEdgeEffect.CurrentTechnique.Passes[0].Apply(); graphicsDevice.SetVertexBuffer(wall.WallEdgeBuffer); graphicsDevice.DrawPrimitives(PrimitiveType.TriangleList, 0, (int)Math.Floor(wall.WallEdgeBuffer.VertexCount / 3.0f)); - - if (destructibleWall.Damage <= 0.0f) { continue; } - wallEdgeEffect.Texture = level.GenerationParams.WallSpriteDestroyed.Texture; - wallEdgeEffect.Alpha = MathHelper.Lerp(0.2f, 1.0f, destructibleWall.Damage / destructibleWall.MaxHealth) * wall.Alpha; - wallEdgeEffect.World = wall.GetTransform() * transformMatrix; - wallEdgeEffect.CurrentTechnique.Passes[0].Apply(); - graphicsDevice.SetVertexBuffer(wall.WallEdgeBuffer); - graphicsDevice.DrawPrimitives(PrimitiveType.TriangleList, 0, (int)Math.Floor(wall.WallEdgeBuffer.VertexCount / 3.0f)); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs index 639b68d1b..6565af460 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs @@ -132,7 +132,7 @@ namespace Barotrauma.Lights public void AddLight(LightSource light) { - if (!lights.Contains(light)) lights.Add(light); + if (!lights.Contains(light)) { lights.Add(light); } } public void RemoveLight(LightSource light) @@ -153,7 +153,7 @@ namespace Barotrauma.Lights public void Update(float deltaTime) { - foreach (LightSource light in lights) + foreach (LightSource light in activeLights) { if (!light.Enabled) { continue; } light.Update(deltaTime); @@ -183,7 +183,7 @@ namespace Barotrauma.Lights foreach (LightSource light in lights) { if (!light.Enabled) { continue; } - if ((light.Color.A < 1 || light.Range < 1.0f || light.CurrentBrightness <= 0.0f) && !light.LightSourceParams.OverrideLightSpriteAlpha.HasValue) { continue; } + if ((light.Color.A < 1 || light.Range < 1.0f) && !light.LightSourceParams.OverrideLightSpriteAlpha.HasValue) { continue; } if (light.ParentBody != null) { light.Position = light.ParentBody.DrawPosition; @@ -212,7 +212,7 @@ namespace Barotrauma.Lights spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, transformMatrix: spriteBatchTransform); foreach (LightSource light in activeLights) { - if (light.IsBackground) { continue; } + if (light.IsBackground || light.CurrentBrightness <= 0.0f) { continue; } //draw limb lights at this point, because they were skipped over previously to prevent them from being obstructed if (light.ParentBody?.UserData is Limb limb && !limb.Hide) { light.DrawSprite(spriteBatch, cam); } } @@ -227,7 +227,7 @@ namespace Barotrauma.Lights Level.Loaded?.BackgroundCreatureManager?.DrawLights(spriteBatch, cam); foreach (LightSource light in activeLights) { - if (!light.IsBackground) { continue; } + if (!light.IsBackground || light.CurrentBrightness <= 0.0f) { continue; } light.DrawSprite(spriteBatch, cam); light.DrawLightVolume(spriteBatch, lightEffect, transform); } @@ -272,7 +272,7 @@ namespace Barotrauma.Lights foreach (LightSource light in activeLights) { //don't draw limb lights at this point, they need to be drawn after lights have been obstructed by characters - if (light.IsBackground || light.ParentBody?.UserData is Limb) { continue; } + if (light.IsBackground || light.ParentBody?.UserData is Limb || light.CurrentBrightness <= 0.0f) { continue; } light.DrawSprite(spriteBatch, cam); } spriteBatch.End(); @@ -337,7 +337,7 @@ namespace Barotrauma.Lights foreach (LightSource light in activeLights) { - if (light.IsBackground) { continue; } + if (light.IsBackground || light.CurrentBrightness <= 0.0f) { continue; } light.DrawLightVolume(spriteBatch, lightEffect, transform); } @@ -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/Lights/LightSource.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs index 42f1bf020..f02802fb9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs @@ -470,37 +470,29 @@ namespace Barotrauma.Lights public void Update(float deltaTime) { + float brightness = 1.0f; if (lightSourceParams.BlinkFrequency > 0.0f) { blinkTimer = (blinkTimer + deltaTime * lightSourceParams.BlinkFrequency) % 1.0f; + if (blinkTimer > 0.5f) + { + CurrentBrightness = 0.0f; + return; + } } - - if (lightSourceParams.PulseFrequency > 0.0f) + if (lightSourceParams.PulseFrequency > 0.0f && lightSourceParams.PulseAmount > 0.0f) { pulseState = (pulseState + deltaTime * lightSourceParams.PulseFrequency) % 1.0f; + //oscillate between 0-1 + brightness *= 1.0f - (float)(Math.Sin(pulseState * MathHelper.TwoPi) + 1.0f) / 2.0f * lightSourceParams.PulseAmount; } - - if (blinkTimer > 0.5f) + if (lightSourceParams.Flicker > 0.0f) { - CurrentBrightness = 0.0f; - } - else - { - float flicker = 0.0f; - float pulse = 0.0f; - if (lightSourceParams.Flicker > 0.0f) - { - flickerState += deltaTime * lightSourceParams.FlickerSpeed; - flickerState %= 255; - flicker = PerlinNoise.GetPerlin(flickerState, flickerState * 0.5f) * lightSourceParams.Flicker; - } - if (lightSourceParams.PulseFrequency > 0.0f && lightSourceParams.PulseAmount > 0.0f) - { - //oscillate between 0-1 - pulse = (float)(Math.Sin(pulseState * MathHelper.TwoPi) + 1.0f) / 2.0f * lightSourceParams.PulseAmount; - } - CurrentBrightness = (1.0f - flicker) * (1.0f - pulse); + flickerState += deltaTime * lightSourceParams.FlickerSpeed; + flickerState %= 255; + brightness *= 1.0f - PerlinNoise.GetPerlin(flickerState, flickerState * 0.5f) * lightSourceParams.Flicker; } + CurrentBrightness = brightness; } /// diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs index 188761e82..041e145d5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs @@ -2,8 +2,8 @@ using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; +using Microsoft.Xna.Framework.Input; namespace Barotrauma { @@ -63,6 +63,8 @@ namespace Barotrauma private Sprite[,] mapTiles; private bool[,] tileDiscovered; + private float connectionHighlightState; + private Pair connectionTooltip; #if DEBUG @@ -120,7 +122,6 @@ namespace Barotrauma DrawOffset = -CurrentLocation.MapPosition; } - Vector2 tileSize = generationParams.MapTiles.Values.First().First().size * generationParams.MapTileScale; int tilesX = (int)Math.Ceiling(Width / tileSize.X); int tilesY = (int)Math.Ceiling(Height / tileSize.Y); @@ -222,7 +223,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()) { @@ -267,15 +268,37 @@ namespace Barotrauma } } - currLocationIndicatorPos = Vector2.Lerp(currLocationIndicatorPos, CurrentDisplayLocation.MapPosition, deltaTime); + Vector2 currentPosition = CurrentDisplayLocation.MapPosition; + if (Level.Loaded?.Type == LevelData.LevelType.LocationConnection && Level.Loaded.StartLocation != null && Level.Loaded.EndLocation != null) + { + Vector2 startPos = CurrentDisplayLocation == Level.Loaded.StartLocation ? Level.Loaded.StartLocation.MapPosition : Level.Loaded.EndLocation.MapPosition; + int moveDir = CurrentDisplayLocation == Level.Loaded.StartLocation ? 1 : -1; + + Vector2 diff = Level.Loaded.EndLocation.MapPosition - Level.Loaded.StartLocation.MapPosition; + currentPosition = startPos + + Vector2.Normalize(diff) * Math.Min(100, diff.Length() * 0.2f) * moveDir; + } + else + { + currentPosition += Vector2.UnitY * 35; + } + + currLocationIndicatorPos = Vector2.Lerp(currLocationIndicatorPos, currentPosition, deltaTime); #if DEBUG if (GameMain.DebugDraw) { if (editor == null) CreateEditor(); editor.AddToGUIUpdateList(order: 1); } + + if (PlayerInput.KeyHit(Keys.Space)) + { + Radiation?.OnStep(); + } #endif + Radiation?.MapUpdate(deltaTime); + if (mapAnimQueue.Count > 0) { hudVisibility = Math.Max(hudVisibility - deltaTime, 0.0f); @@ -324,6 +347,15 @@ namespace Barotrauma } } + if (SelectedConnection != null) + { + connectionHighlightState = Math.Min(connectionHighlightState + deltaTime, 1.0f); + } + else + { + connectionHighlightState = 0.0f; + } + if (GUI.KeyboardDispatcher.Subscriber == null) { float moveSpeed = 1000.0f; @@ -336,7 +368,7 @@ namespace Barotrauma } targetZoom = MathHelper.Clamp(targetZoom, generationParams.MinZoom, generationParams.MaxZoom); - zoom = MathHelper.Lerp(zoom, targetZoom, 0.1f); + zoom = MathHelper.Lerp(zoom, targetZoom * GUI.Scale, 0.1f); if (GUI.MouseOn == mapContainer) { @@ -351,6 +383,7 @@ namespace Barotrauma //clients aren't allowed to select the location without a permission if ((GameMain.GameSession?.GameMode as CampaignMode)?.AllowedToManageCampaign() ?? false) { + connectionHighlightState = 0.0f; SelectedConnection = connection; SelectedLocation = HighlightedLocation; @@ -383,12 +416,13 @@ namespace Barotrauma Level.Loaded.DebugSetEndLocation(null); CurrentLocation.Discovered = true; - CurrentLocation.CreateStore(); OnLocationChanged?.Invoke(prevLocation, CurrentLocation); SelectLocation(-1); if (GameMain.Client == null) { + CurrentLocation.CreateStore(); ProgressWorld(); + Radiation.OnStep(1); } else { @@ -483,6 +517,8 @@ namespace Barotrauma float rawNoiseScale = 1.0f + PerlinNoise.GetPerlin((int)(Timing.TotalTime * 1 - 1), (int)(Timing.TotalTime * 1 - 1)); cameraNoiseStrength = PerlinNoise.GetPerlin((int)(Timing.TotalTime * 1 - 1), (int)(Timing.TotalTime * 1 - 1)); + Radiation.Draw(spriteBatch, rect, zoom); + noiseOverlay.DrawTiled(spriteBatch, rect.Location.ToVector2(), rect.Size.ToVector2(), startOffset: new Vector2(Rand.Range(0.0f, noiseOverlay.SourceRect.Width), Rand.Range(0.0f, noiseOverlay.SourceRect.Height)), color : Color.White * cameraNoiseStrength * 0.1f, @@ -519,22 +555,6 @@ namespace Barotrauma if (!rect.Intersects(drawRect)) { continue; } - if (location == CurrentDisplayLocation ) - { - generationParams.CurrentLocationIndicator.Draw(spriteBatch, - rectCenter + (currLocationIndicatorPos + viewOffset) * zoom, - generationParams.IndicatorColor, - generationParams.CurrentLocationIndicator.Origin, 0, Vector2.One * (generationParams.LocationIconSize / generationParams.CurrentLocationIndicator.size.X) * 1.7f * zoom); - } - - if (location == SelectedLocation) - { - generationParams.SelectedLocationIndicator.Draw(spriteBatch, - rectCenter + (location.MapPosition + viewOffset) * zoom, - generationParams.IndicatorColor, - generationParams.SelectedLocationIndicator.Origin, 0, Vector2.One * (generationParams.LocationIconSize / generationParams.SelectedLocationIndicator.size.X) * 1.7f * zoom); - } - Color color = location.Type.SpriteColor; if (!location.Discovered) { color = Color.White; } if (location.Connections.Find(c => c.Locations.Contains(CurrentDisplayLocation)) == null) @@ -542,6 +562,12 @@ namespace Barotrauma color *= 0.5f; } + // TODO proper visualization of this + if (location.Type.HasOutpost && !location.HasOutpost()) + { + color = GUI.Style.Red; + } + float iconScale = location == CurrentDisplayLocation ? 1.2f : 1.0f; if (location == HighlightedLocation) { @@ -550,7 +576,35 @@ 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 == CurrentDisplayLocation) + { + if (SelectedLocation != null) + { + Vector2 dir = Vector2.Normalize(SelectedLocation.MapPosition - currLocationIndicatorPos); + GUI.Arrow.Draw(spriteBatch, + rectCenter + (currLocationIndicatorPos + viewOffset) * zoom + dir * generationParams.LocationIconSize * 0.6f * zoom, + generationParams.IndicatorColor, + GUI.Arrow.Origin, + rotate: MathUtils.VectorToAngle(dir) + MathHelper.PiOver2, + new Vector2(0.5f, 1.0f) * zoom); + } + generationParams.CurrentLocationIndicator.Draw(spriteBatch, + rectCenter + (currLocationIndicatorPos + viewOffset) * zoom, + generationParams.IndicatorColor, + generationParams.CurrentLocationIndicator.Origin, 0, Vector2.One * (generationParams.LocationIconSize / generationParams.CurrentLocationIndicator.size.X) * 0.8f * zoom); + + } + + if (location == SelectedLocation) + { + generationParams.SelectedLocationIndicator.Draw(spriteBatch, + rectCenter + (location.MapPosition + viewOffset) * zoom, + generationParams.IndicatorColor, + generationParams.SelectedLocationIndicator.Origin, 0, Vector2.One * (generationParams.LocationIconSize / generationParams.SelectedLocationIndicator.size.X) * 1.7f * zoom); + } + + 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; @@ -576,7 +630,7 @@ namespace Barotrauma } } - if (GameMain.DebugDraw && location == HighlightedLocation && (!location.Discovered || !location.Type.HasOutpost)) + if (GameMain.DebugDraw && location == HighlightedLocation && (!location.Discovered || !location.HasOutpost())) { if (location.Reputation != null) { @@ -608,9 +662,9 @@ namespace Barotrauma Vector2 pos = rectCenter + (HighlightedLocation.MapPosition + viewOffset) * zoom; pos.X += 50 * zoom; Vector2 nameSize = GUI.LargeFont.MeasureString(HighlightedLocation.Name); - Vector2 typeSize = GUI.Font.MeasureString(HighlightedLocation.Type.Name); + Vector2 typeSize = string.IsNullOrEmpty(HighlightedLocation.Type.Name) ? Vector2.Zero : 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) @@ -674,19 +728,22 @@ namespace Barotrauma int width = (int)(generationParams.LocationConnectionWidth * zoom); + //current level if (Level.Loaded?.LevelData == connection.LevelData) { connectionColor = generationParams.HighlightedConnectionColor; width = (int)(width * 1.5f); } + //selected connection if (SelectedLocation != CurrentDisplayLocation && - (connection.Locations.Contains(SelectedLocation) && connection.Locations.Contains(CurrentDisplayLocation))) + connection.Locations.Contains(SelectedLocation) && connection.Locations.Contains(CurrentDisplayLocation)) { connectionColor = generationParams.HighlightedConnectionColor; width *= 2; } + //highlighted connection else if (HighlightedLocation != CurrentDisplayLocation && - (connection.Locations.Contains(HighlightedLocation) && connection.Locations.Contains(CurrentDisplayLocation))) + connection.Locations.Contains(HighlightedLocation) && connection.Locations.Contains(CurrentDisplayLocation)) { connectionColor = generationParams.HighlightedConnectionColor; width *= 2; @@ -741,47 +798,90 @@ namespace Barotrauma } float dist = Vector2.Distance(start, end); var connectionSprite = connection.Passed ? generationParams.PassedConnectionSprite : generationParams.ConnectionSprite; + + Color segmentColor = connectionColor; + int segmentWidth = width; + if (connection == SelectedConnection) + { + float t = (i - startIndex) / (float)(endIndex - startIndex - 1); + if (CurrentDisplayLocation == connection.Locations[1]) { t = 1.0f - t; } + if (t > connectionHighlightState) + { + segmentWidth /= 2; + segmentColor = connection.Passed ? generationParams.ConnectionColor : generationParams.UnvisitedConnectionColor; + } + else + { + } + } + spriteBatch.Draw(connectionSprite.Texture, - new Rectangle((int)start.X, (int)start.Y, (int)(dist - 1 * zoom), width), - connectionSprite.SourceRect, connectionColor * a, MathUtils.VectorToAngle(end - start), + new Rectangle((int)start.X, (int)start.Y, (int)(dist - 1 * zoom), segmentWidth), + connectionSprite.SourceRect, segmentColor * a, + MathUtils.VectorToAngle(end - start), new Vector2(0, connectionSprite.size.Y / 2), SpriteEffects.None, 0.01f); } + + int iconCount = 0, iconIndex = 0; if (connectionStart.HasValue && connectionEnd.HasValue) - { - GUIComponentStyle crushDepthWarningIconStyle = null; + { + if (connection.LevelData.HasBeaconStation) { iconCount++; } + if (connection.LevelData.HasHuntingGrounds) { iconCount++; } string tooltip = null; - if (connection.LevelData.InitialDepth * Physics.DisplayToRealWorldRatio > connection.LevelData.RealWorldCrushDepth) + var subCrushDepth = Submarine.MainSub?.RealWorldCrushDepth ?? Level.DefaultRealWorldCrushDepth; + if (GameMain.GameSession?.Campaign?.UpgradeManager != null) { - crushDepthWarningIconStyle = GUI.Style.GetComponentStyle("CrushDepthWarningHighIcon"); + var hullUpgradePrefab = UpgradePrefab.Find("increasewallhealth"); + if (hullUpgradePrefab != null) + { + int pendingLevel = GameMain.GameSession.Campaign.UpgradeManager.GetUpgradeLevel(hullUpgradePrefab, hullUpgradePrefab.UpgradeCategories.First()); + int currentLevel = GameMain.GameSession.Campaign.UpgradeManager.GetRealUpgradeLevel(hullUpgradePrefab, hullUpgradePrefab.UpgradeCategories.First()); + if (pendingLevel > currentLevel) + { + string updateValueStr = hullUpgradePrefab.SourceElement?.Element("Structure")?.GetAttributeString("crushdepth", null); + if (!string.IsNullOrEmpty(updateValueStr)) + { + subCrushDepth = PropertyReference.CalculateUpgrade(subCrushDepth, pendingLevel - currentLevel, updateValueStr); + } + } + } + } + + string crushDepthWarningIconStyle = null; + if (connection.LevelData.InitialDepth * Physics.DisplayToRealWorldRatio > subCrushDepth) + { + iconCount++; + crushDepthWarningIconStyle = "CrushDepthWarningHighIcon"; tooltip = "crushdepthwarninghigh"; } - else if ((connection.LevelData.InitialDepth + connection.LevelData.Size.Y) * Physics.DisplayToRealWorldRatio > connection.LevelData.RealWorldCrushDepth) + else if ((connection.LevelData.InitialDepth + connection.LevelData.Size.Y) * Physics.DisplayToRealWorldRatio > subCrushDepth) { - crushDepthWarningIconStyle = GUI.Style.GetComponentStyle("CrushDepthWarningLowIcon"); + iconCount++; + crushDepthWarningIconStyle = "CrushDepthWarningLowIcon"; tooltip = "crushdepthwarninglow"; } + if (connection.LevelData.HasBeaconStation) + { + var beaconStationIconStyle = connection.LevelData.IsBeaconActive ? "BeaconStationActive" : "BeaconStationInactive"; + DrawIcon(beaconStationIconStyle, (int)(28 * zoom), TextManager.Get(connection.LevelData.IsBeaconActive ? "BeaconStationActiveTooltip" : "BeaconStationInactiveTooltip")); + } + + if (connection.LevelData.HasHuntingGrounds) + { + DrawIcon("HuntingGrounds", (int)(28 * zoom), TextManager.Get("HuntingGroundsTooltip")); + } + if (crushDepthWarningIconStyle != null) { - Vector2 iconPos = (connectionStart.Value + connectionEnd.Value) / 2; - float iconSize = 32.0f * GUI.Scale; - bool mouseOn = HighlightedLocation == null && Vector2.DistanceSquared(iconPos, PlayerInput.MousePosition) < iconSize * iconSize; - Sprite crushDepthWarningIcon = crushDepthWarningIconStyle.GetDefaultSprite(); - crushDepthWarningIcon.Draw(spriteBatch, iconPos, - mouseOn ? crushDepthWarningIconStyle.HoverColor : crushDepthWarningIconStyle.Color, - scale: iconSize / crushDepthWarningIcon.size.X); - if (mouseOn) - { - connectionTooltip = new Pair( - new Rectangle(iconPos.ToPoint(), new Point((int)iconSize)), - TextManager.Get(tooltip) + DrawIcon(crushDepthWarningIconStyle, (int)(32 * zoom), + TextManager.Get(tooltip) .Replace("[initialdepth]", ((int)(connection.LevelData.InitialDepth * Physics.DisplayToRealWorldRatio)).ToString()) - .Replace("[submarinecrushdepth]", ((int)(Submarine.MainSub?.RealWorldCrushDepth ?? Level.DefaultRealWorldCrushDepth)).ToString())); - } + .Replace("[submarinecrushdepth]", ((int)subCrushDepth).ToString())); } } - if (GameMain.DebugDraw && zoom > 1.0f && generationParams.ShowLevelTypeNames) + if (GameMain.DebugDraw && zoom > (1.0f * GUI.Scale) && generationParams.ShowLevelTypeNames) { Vector2 center = rectCenter + (connection.CenterPos + viewOffset) * zoom; if (viewArea.Contains(center) && connection.Biome != null) @@ -789,6 +889,25 @@ namespace Barotrauma GUI.DrawString(spriteBatch, center, connection.Biome.Identifier + " (" + connection.Difficulty + ")", Color.White); } } + + void DrawIcon(string iconStyle, int iconSize, string tooltip) + { + Vector2 iconPos = (connectionStart.Value + connectionEnd.Value) / 2; + Vector2 iconDiff = Vector2.Normalize(connectionEnd.Value - connectionStart.Value) * iconSize; + + iconPos += (iconDiff * -(iconCount - 1) / 2.0f) + iconDiff * iconIndex; + + var style = GUI.Style.GetComponentStyle(iconStyle); + bool mouseOn = HighlightedLocation == null && Vector2.DistanceSquared(iconPos, PlayerInput.MousePosition) < iconSize * iconSize; + Sprite iconSprite = style.GetDefaultSprite(); + iconSprite.Draw(spriteBatch, iconPos, (mouseOn ? style.HoverColor : style.Color) * 0.7f, + scale: iconSize / iconSprite.size.X); + if (mouseOn) + { + connectionTooltip = new Pair(new Rectangle(iconPos.ToPoint(), new Point(iconSize)), tooltip); + } + iconIndex++; + } } private float hudVisibility; @@ -819,8 +938,9 @@ namespace Barotrauma return; } - if (anim.StartZoom == null) { anim.StartZoom = MathUtils.InverseLerp(generationParams.MinZoom, generationParams.MaxZoom, zoom); } - if (anim.EndZoom == null) { anim.EndZoom = MathUtils.InverseLerp(generationParams.MinZoom, generationParams.MaxZoom, zoom); } + float unscaledZoom = zoom / GUI.Scale; + if (anim.StartZoom == null) { anim.StartZoom = MathUtils.InverseLerp(generationParams.MinZoom, generationParams.MaxZoom, unscaledZoom); } + if (anim.EndZoom == null) { anim.EndZoom = MathUtils.InverseLerp(generationParams.MinZoom, generationParams.MaxZoom, unscaledZoom); } anim.StartPos = (anim.StartLocation == null) ? -DrawOffset : anim.StartLocation.MapPosition; @@ -833,7 +953,8 @@ namespace Barotrauma zoom = MathHelper.Lerp(generationParams.MinZoom, generationParams.MaxZoom, - MathHelper.SmoothStep(anim.StartZoom.Value, anim.EndZoom.Value, t)); + MathHelper.SmoothStep(anim.StartZoom.Value, anim.EndZoom.Value, t)) + * GUI.Scale; if (anim.Timer >= anim.Duration) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Radiation.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Radiation.cs new file mode 100644 index 000000000..1fce33720 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Radiation.cs @@ -0,0 +1,38 @@ +#nullable enable +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace Barotrauma +{ + internal partial class Radiation + { + public void Draw(SpriteBatch spriteBatch, Rectangle container, float zoom) + { + if (!Enabled) { return; } + + UISprite uiSprite = GUI.Style.RadiationSprite; + var (offsetX, offsetY) = Map.DrawOffset * zoom; + var (centerX, centerY) = container.Center.ToVector2(); + var (halfSizeX, halfSizeY) = new Vector2(container.Width / 2f, container.Height / 2f) * zoom; + float viewBottom = centerY + Map.Height * zoom; + Vector2 topLeft = new Vector2(centerX + offsetX - halfSizeX, centerY + offsetY - halfSizeY); + Vector2 size = new Vector2((Amount - increasedAmount) * zoom + halfSizeX, viewBottom - topLeft.Y); + if (size.X < 0) { return; } + + uiSprite.Sprite.DrawTiled(spriteBatch, topLeft, size, GUI.Style.Red * 0.33f, Vector2.Zero, textureScale: new Vector2(zoom)); + + if (container.Contains(PlayerInput.MousePosition) && PlayerInput.MousePosition.X < topLeft.X + size.X) + { + // TODO tooltip? + } + } + + public void MapUpdate(float deltaTime) + { + if (increasedAmount > 0) + { + increasedAmount -= (lastIncrease / Params.AnimationSpeed) * deltaTime; + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs index b9748ef25..b9cbf543f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs @@ -114,6 +114,18 @@ namespace Barotrauma public MapEntity ReplacedBy; public virtual void Draw(SpriteBatch spriteBatch, bool editing, bool back = true) { } + + /// + /// A method that modifies the draw depth to prevent z-fighting between entities with the same sprite depth + /// + public float GetDrawDepth(float baseDepth, Sprite sprite) + { + float depth = baseDepth + //take texture into account to get entities with (roughly) the same base depth and texture to render consecutively to minimize texture swaps + + (sprite?.Texture?.SortingKey ?? 0) % 100 * 0.00001f + + ID % 100 * 0.000001f; + return Math.Min(depth, 1.0f); + } /// /// Update the selection logic in submarine editor @@ -202,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)) { @@ -267,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 @@ -464,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 @@ -479,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; @@ -499,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 f8a64e70a..4e8b32ca9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -24,7 +24,7 @@ namespace Barotrauma { get { - if (!GameMain.SubEditorScreen.ShowThalamus && prefab.Category.HasFlag(MapEntityCategory.Thalamus)) + if (GameMain.SubEditorScreen.IsSubcategoryHidden(prefab.Subcategory)) { return false; } @@ -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,11 +221,14 @@ namespace Barotrauma Draw(spriteBatch, editing, false, damageEffect); } + private float GetRealDepth() + { + return SpriteDepthOverrideIsSet ? SpriteOverrideDepth : prefab.sprite.Depth; + } + public float GetDrawDepth() { - float depth = SpriteDepthOverrideIsSet ? SpriteOverrideDepth : prefab.sprite.Depth; - depth -= (ID % 255) * 0.000001f; - return depth; + return GetDrawDepth(GetRealDepth(), prefab.sprite); } private void Draw(SpriteBatch spriteBatch, bool editing, bool back = true, Effect damageEffect = null) @@ -254,7 +258,7 @@ namespace Barotrauma thickness: Math.Max(1, (int)(2 / Screen.Selected.Cam.Zoom))); } - bool isWiringMode = editing && SubEditorScreen.IsWiringMode(); + bool isWiringMode = editing && SubEditorScreen.TransparentWiringMode && SubEditorScreen.IsWiringMode(); if (isWiringMode) { color *= 0.15f; } @@ -263,8 +267,8 @@ namespace Barotrauma float depth = GetDrawDepth(); Vector2 textureOffset = this.textureOffset; - if (FlippedX) textureOffset.X = -textureOffset.X; - if (FlippedY) textureOffset.Y = -textureOffset.Y; + if (FlippedX) { textureOffset.X = -textureOffset.X; } + if (FlippedY) { textureOffset.Y = -textureOffset.Y; } if (back && damageEffect == null && !isWiringMode) { @@ -304,7 +308,7 @@ namespace Barotrauma color: Prefab.BackgroundSpriteColor, textureScale: TextureScale * Scale, startOffset: backGroundOffset, - depth: Math.Max(Prefab.BackgroundSprite.Depth + (ID % 255) * 0.000001f, depth + 0.000001f)); + depth: Math.Max(GetDrawDepth(Prefab.BackgroundSprite.Depth, Prefab.BackgroundSprite), depth + 0.000001f)); if (UseDropShadow) { @@ -322,13 +326,14 @@ namespace Barotrauma } } - if (back == depth > 0.5f) + if (back == GetRealDepth() > 0.5f) { SpriteEffects oldEffects = prefab.sprite.effects; prefab.sprite.effects ^= SpriteEffects; for (int i = 0; i < Sections.Length; i++) { + Rectangle drawSection = Sections[i].rect; if (damageEffect != null) { float newCutoff = MathHelper.Lerp(0.0f, 0.65f, Sections[i].damage / MaxHealth); @@ -345,21 +350,30 @@ namespace Barotrauma Submarine.DamageEffectColor = color; } } + if (!HasDamage && i == 0) + { + drawSection = new Rectangle( + drawSection.X, + drawSection.Y, + Sections[Sections.Length -1 ].rect.Right - drawSection.X, + drawSection.Y - (Sections[Sections.Length - 1].rect.Y - Sections[Sections.Length - 1].rect.Height)); + i = Sections.Length; + } Vector2 sectionOffset = new Vector2( - Math.Abs(rect.Location.X - Sections[i].rect.Location.X), - Math.Abs(rect.Location.Y - Sections[i].rect.Location.Y)); + Math.Abs(rect.Location.X - drawSection.Location.X), + Math.Abs(rect.Location.Y - drawSection.Location.Y)); - if (FlippedX && IsHorizontal) sectionOffset.X = Sections[i].rect.Right - rect.Right; - if (FlippedY && !IsHorizontal) sectionOffset.Y = (rect.Y - rect.Height) - (Sections[i].rect.Y - Sections[i].rect.Height); + if (FlippedX && IsHorizontal) { sectionOffset.X = drawSection.Right - rect.Right; } + if (FlippedY && !IsHorizontal) { sectionOffset.Y = (rect.Y - rect.Height) - (drawSection.Y - drawSection.Height); } sectionOffset.X += MathUtils.PositiveModulo((int)-textureOffset.X, prefab.sprite.SourceRect.Width); sectionOffset.Y += MathUtils.PositiveModulo((int)-textureOffset.Y, prefab.sprite.SourceRect.Height); prefab.sprite.DrawTiled( spriteBatch, - new Vector2(Sections[i].rect.X + drawOffset.X, -(Sections[i].rect.Y + drawOffset.Y)), - new Vector2(Sections[i].rect.Width, Sections[i].rect.Height), + new Vector2(drawSection.X + drawOffset.X, -(drawSection.Y + drawOffset.Y)), + new Vector2(drawSection.Width, drawSection.Height), color: color, startOffset: sectionOffset, depth: depth, @@ -369,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/Map/SubmarineBody.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineBody.cs index e271e9138..2a63d6690 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineBody.cs @@ -79,14 +79,22 @@ namespace Barotrauma if (maxDamageStructure != null) { - SoundPlayer.PlayDamageSound( - soundTag, - impact * 10.0f, - ConvertUnits.ToDisplayUnits(impactSimPos), - MathHelper.Lerp(2000.0f, 10000.0f, (impact - MinCollisionImpact) / 2.0f), - maxDamageStructure.Tags); + PlayDamageSound(impactSimPos, impact, soundTag, maxDamageStructure); } } + private void PlayDamageSound(Vector2 impactSimPos, float impact, string soundTag, Structure hitStructure = null) + { + if (impact < MinCollisionImpact) { return; } + + SoundPlayer.PlayDamageSound( + soundTag, + impact * 10.0f, + ConvertUnits.ToDisplayUnits(impactSimPos), + MathHelper.Lerp(2000.0f, 10000.0f, (impact - MinCollisionImpact) / 2.0f), + hitStructure?.Tags); + } + + } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs index c9444afcb..898d39d8c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/BanList.cs @@ -3,12 +3,13 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; +using System.Text; namespace Barotrauma.Networking { partial class BannedPlayer { - public BannedPlayer(string name, UInt16 uniqueIdentifier, bool isRangeBan, string endPoint, ulong steamID) + public BannedPlayer(string name, UInt16 uniqueIdentifier, bool isRangeBan, string endPoint, ulong steamID, string reason, DateTime? expiration) { this.Name = name; this.EndPoint = endPoint; @@ -16,6 +17,8 @@ namespace Barotrauma.Networking ParseEndPointAsSteamId(); this.IsRangeBan = isRangeBan; this.UniqueIdentifier = uniqueIdentifier; + this.Reason = reason; + this.ExpirationTime = expiration; } } @@ -152,12 +155,24 @@ namespace Barotrauma.Networking bannedPlayers.Clear(); UInt32 bannedPlayerCount = incMsg.ReadVariableUInt32(); + for (int i = 0; i < (int)bannedPlayerCount; i++) { string name = incMsg.ReadString(); UInt16 uniqueIdentifier = incMsg.ReadUInt16(); - bool isRangeBan = incMsg.ReadBoolean(); incMsg.ReadPadBits(); - + bool isRangeBan = incMsg.ReadBoolean(); + bool includesExpiration = incMsg.ReadBoolean(); + incMsg.ReadPadBits(); + + DateTime? expiration = null; + if (includesExpiration) + { + double hoursFromNow = incMsg.ReadDouble(); + expiration = DateTime.Now + TimeSpan.FromHours(hoursFromNow); + } + + string reason = incMsg.ReadString(); + string endPoint = ""; UInt64 steamID = 0; if (isOwner) @@ -170,7 +185,7 @@ namespace Barotrauma.Networking endPoint = "Endpoint concealed by host"; steamID = 0; } - bannedPlayers.Add(new BannedPlayer(name, uniqueIdentifier, isRangeBan, endPoint, steamID)); + bannedPlayers.Add(new BannedPlayer(name, uniqueIdentifier, isRangeBan, endPoint, steamID, reason, expiration)); } if (banFrame != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs index f0cf3371c..75073f197 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs @@ -48,7 +48,54 @@ namespace Barotrauma.Networking UInt16 targetCharacterID = msg.ReadUInt16(); Character targetCharacter = Entity.FindEntityByID(targetCharacterID) as Character; Entity targetEntity = Entity.FindEntityByID(msg.ReadUInt16()); - int optionIndex = msg.ReadByte(); + + Order orderPrefab = null; + int? optionIndex = null; + string orderOption = null; + + // The option of a Dismiss order is written differently so we know what order we target + // now that the game supports multiple current orders simultaneously + if (orderIndex >= 0 && orderIndex < Order.PrefabList.Count) + { + orderPrefab = Order.PrefabList[orderIndex]; + if (orderPrefab.Identifier != "dismissed") + { + optionIndex = msg.ReadByte(); + } + // Does the dismiss order have a specified target? + else if (msg.ReadBoolean()) + { + int identifierCount = msg.ReadByte(); + if (identifierCount > 0) + { + int dismissedOrderIndex = msg.ReadByte(); + Order dismissedOrderPrefab = null; + if (dismissedOrderIndex >= 0 && dismissedOrderIndex < Order.PrefabList.Count) + { + dismissedOrderPrefab = Order.PrefabList[dismissedOrderIndex]; + orderOption = dismissedOrderPrefab.Identifier; + } + if (identifierCount > 1) + { + int dismissedOrderOptionIndex = msg.ReadByte(); + if (dismissedOrderPrefab != null) + { + var options = dismissedOrderPrefab.Options; + if (options != null && dismissedOrderOptionIndex >= 0 && dismissedOrderOptionIndex < options.Length) + { + orderOption += $".{options[dismissedOrderOptionIndex]}"; + } + } + } + } + } + } + else + { + optionIndex = msg.ReadByte(); + } + + int orderPriority = msg.ReadByte(); OrderTarget orderTargetPosition = null; Order.OrderTargetType orderTargetType = (Order.OrderTargetType)msg.ReadByte(); int wallSectionIndex = 0; @@ -64,7 +111,6 @@ namespace Barotrauma.Networking wallSectionIndex = msg.ReadByte(); } - Order orderPrefab; if (orderIndex < 0 || orderIndex >= Order.PrefabList.Count) { DebugConsole.ThrowError("Invalid order message - order index out of bounds."); @@ -73,13 +119,10 @@ namespace Barotrauma.Networking } else { - orderPrefab = Order.PrefabList[orderIndex]; - } - string orderOption = ""; - if (optionIndex >= 0 && optionIndex < orderPrefab.Options.Length) - { - orderOption = orderPrefab.Options[optionIndex]; + orderPrefab ??= Order.PrefabList[orderIndex]; } + + orderOption ??= optionIndex.HasValue && optionIndex >= 0 && optionIndex < orderPrefab.Options.Length ? orderPrefab.Options[optionIndex.Value] : ""; txt = orderPrefab.GetChatMessage(targetCharacter?.Name, senderCharacter?.CurrentHull?.DisplayName, givingOrderToSelf: targetCharacter == senderCharacter, orderOption: orderOption); if (GameMain.Client.GameStarted && Screen.Selected == GameMain.GameScreen) @@ -107,7 +150,7 @@ namespace Barotrauma.Networking } else if (targetCharacter != null) { - targetCharacter.SetOrder(order, orderOption, senderCharacter); + targetCharacter.SetOrder(order, orderOption, orderPriority, senderCharacter); } } } @@ -115,7 +158,7 @@ namespace Barotrauma.Networking if (NetIdUtils.IdMoreRecent(ID, LastID)) { GameMain.Client.AddChatMessage( - new OrderChatMessage(orderPrefab, orderOption, txt, orderTargetPosition ?? targetEntity as ISpatialEntity, targetCharacter, senderCharacter)); + new OrderChatMessage(orderPrefab, orderOption, orderPriority, txt, orderTargetPosition ?? targetEntity as ISpatialEntity, targetCharacter, senderCharacter)); LastID = ID; } return; 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 47fafd5f4..e0b96af1d 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 { @@ -272,6 +278,7 @@ namespace Barotrauma.Networking private void ConnectToServer(object endpoint, string hostName) { LastClientListUpdateID = 0; + foreach (var c in ConnectedClients) { GameMain.NetLobbyScreen.RemovePlayer(c); @@ -361,6 +368,14 @@ namespace Barotrauma.Networking { GameMain.NetLobbyScreen.Select(); } + else + { + entityEventManager.ClearSelf(); + foreach (Character c in Character.CharacterList) + { + c.ResetNetState(); + } + } chatBox.InputBox.Enabled = true; if (GameMain.NetLobbyScreen?.ChatInput != null) @@ -842,12 +857,25 @@ namespace Barotrauma.Networking string endMessage = string.Empty; endMessage = inc.ReadString(); - bool missionSuccessful = inc.ReadBoolean(); - Character.TeamType winningTeam = (Character.TeamType)inc.ReadByte(); - if (missionSuccessful && GameMain.GameSession?.Mission != null) + byte missionCount = inc.ReadByte(); + for (int i = 0; i < missionCount; i++) + { + bool missionSuccessful = inc.ReadBoolean(); + var mission = GameMain.GameSession?.GetMission(i); + if (mission != null) + { + mission.Completed = missionSuccessful; + } + } + CharacterTeamType winningTeam = (CharacterTeamType)inc.ReadByte(); + if (winningTeam != CharacterTeamType.None) { GameMain.GameSession.WinningTeam = winningTeam; - GameMain.GameSession.Mission.Completed = true; + var combatMission = GameMain.GameSession.Missions.FirstOrDefault(m => m is CombatMission); + if (combatMission != null) + { + combatMission.Completed = true; + } } byte traitorCount = inc.ReadByte(); @@ -913,7 +941,11 @@ namespace Barotrauma.Networking ReadTraitorMessage(inc); break; case ServerPacketHeader.MISSION: - GameMain.GameSession?.Mission?.ClientRead(inc); + { + int missionIndex = inc.ReadByte(); + Mission mission = GameMain.GameSession?.GetMission(missionIndex); + mission?.ClientRead(inc); + } break; case ServerPacketHeader.EVENTACTION: GameMain.GameSession?.EventManager.ClientRead(inc); @@ -944,17 +976,26 @@ namespace Barotrauma.Networking throw new Exception(errorMsg); } - string missionIdentifier = inc.ReadString() ?? ""; - if (missionIdentifier != (GameMain.GameSession.Mission?.Prefab.Identifier ?? "")) + byte missionCount = inc.ReadByte(); + if (missionCount != GameMain.GameSession.Missions.Count()) { - string errorMsg = $"Mission equality check failed. The mission selected at your end doesn't match the one loaded by the server (server: {missionIdentifier ?? "null"}, client: {GameMain.GameSession.Mission?.Prefab.Identifier ?? ""})"; - GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:MissionsDontMatch" + Level.Loaded.Seed, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + string errorMsg = $"Mission equality check failed. Mission count doesn't match the server (server: {missionCount}, client: {GameMain.GameSession.Missions.Count()})"; throw new Exception(errorMsg); } + foreach (Mission mission in GameMain.GameSession.Missions) + { + string missionIdentifier = inc.ReadString() ?? ""; + if (missionIdentifier != mission.Prefab.Identifier) + { + string errorMsg = $"Mission equality check failed. The mission selected at your end doesn't match the one loaded by the server (server: {missionIdentifier ?? "null"}, client: {mission.Prefab.Identifier})"; + GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:MissionsDontMatch" + Level.Loaded.Seed, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + throw new Exception(errorMsg); + } + } byte equalityCheckValueCount = inc.ReadByte(); List levelEqualityCheckValues = new List(); - for (int i = 0; i missionIndices = new List(); + int missionCount = inc.ReadByte(); + for (int i = 0; i < missionCount; i++) + { + missionIndices.Add(inc.ReadInt16()); + } if (!GameMain.NetLobbyScreen.TrySelectSub(subName, subHash, GameMain.NetLobbyScreen.SubList)) { roundInitStatus = RoundInitStatus.Interrupted; @@ -1471,7 +1520,9 @@ namespace Barotrauma.Networking yield return CoroutineStatus.Failure; } - GameMain.GameSession = new GameSession(GameMain.NetLobbyScreen.SelectedSub, gameMode, missionPrefab: missionIndex < 0 ? null : MissionPrefab.List[missionIndex]); + var selectedMissions = missionIndices.Select(i => MissionPrefab.List[i]); + + GameMain.GameSession = new GameSession(GameMain.NetLobbyScreen.SelectedSub, gameMode, missionPrefabs: selectedMissions); GameMain.GameSession.StartRound(levelSeed, levelDifficulty); } else @@ -1625,7 +1676,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) { @@ -1697,7 +1748,7 @@ namespace Barotrauma.Networking // Enable characters near the main sub for the endCinematic foreach (Character c in Character.CharacterList) { - if (Vector2.DistanceSquared(Submarine.MainSub.WorldPosition, c.WorldPosition) < NetConfig.EnableCharacterDistSqr) + if (Vector2.DistanceSquared(Submarine.MainSub.WorldPosition, c.WorldPosition) < MathUtils.Pow2(c.Params.DisableDistance)) { c.Enabled = true; } @@ -1819,6 +1870,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(); @@ -1835,6 +1887,7 @@ namespace Barotrauma.Networking SteamID = steamId, Name = name, PreferredJob = preferredJob, + PreferredTeam = (CharacterTeamType)preferredTeam, CharacterID = characterID, Karma = karma, Muted = muted, @@ -1869,6 +1922,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; @@ -1908,6 +1962,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) @@ -2133,6 +2198,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); @@ -2147,8 +2213,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 @@ -2221,9 +2294,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)) @@ -2236,7 +2309,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\"!"); } } @@ -2260,6 +2333,7 @@ namespace Barotrauma.Networking { outmsg.Write(""); } + outmsg.Write((byte)GameMain.Config.TeamPreference); if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaign.LastSaveID == 0) { @@ -2287,7 +2361,7 @@ namespace Barotrauma.Networking if (outmsg.LengthBytes > MsgConstants.MTU) { - DebugConsole.ThrowError("Maximum packet size exceeded (" + outmsg.LengthBytes + " > " + MsgConstants.MTU); + DebugConsole.ThrowError($"Maximum packet size exceeded ({outmsg.LengthBytes} > {MsgConstants.MTU})"); } clientPeer.Send(outmsg, DeliveryMethod.Unreliable); @@ -2338,7 +2412,7 @@ namespace Barotrauma.Networking if (outmsg.LengthBytes > MsgConstants.MTU) { - DebugConsole.ThrowError("Maximum packet size exceeded (" + outmsg.LengthBytes + " > " + MsgConstants.MTU); + DebugConsole.ThrowError($"Maximum packet size exceeded ({outmsg.LengthBytes} > {MsgConstants.MTU})"); } clientPeer.Send(outmsg, DeliveryMethod.Unreliable); @@ -2895,8 +2969,9 @@ namespace Barotrauma.Networking return false; } if (button != null) { button.Enabled = false; } - if (campaign != null) LateCampaignJoin = true; + if (campaign != null) { LateCampaignJoin = true; } + if (clientPeer == null) { return false; } IWriteMessage readyToStartMsg = new WriteOnlyMessage(); readyToStartMsg.Write((byte)ClientPacketHeader.RESPONSE_STARTGAME); @@ -3231,12 +3306,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); } @@ -3246,7 +3321,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; } @@ -3328,9 +3403,12 @@ namespace Barotrauma.Networking { var banReasonPrompt = new GUIMessageBox( TextManager.Get(ban ? "BanReasonPrompt" : "KickReasonPrompt"), - "", new string[] { TextManager.Get("OK"), TextManager.Get("Cancel") }, new Vector2(0.25f, 0.22f), new Point(400, 220)); + "", new string[] { TextManager.Get("OK"), TextManager.Get("Cancel") }, new Vector2(0.25f, 0.25f), new Point(400, 260)); - var content = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.6f), banReasonPrompt.InnerFrame.RectTransform, Anchor.Center)); + var content = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.6f), banReasonPrompt.InnerFrame.RectTransform, Anchor.Center)) + { + AbsoluteSpacing = GUI.IntScale(5) + }; var banReasonBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.3f), content.RectTransform)) { Wrap = true, @@ -3341,10 +3419,9 @@ namespace Barotrauma.Networking GUITickBox permaBanTickBox = null; if (ban) - { - + { var labelContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.25f), content.RectTransform), isHorizontal: false); - new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), labelContainer.RectTransform), TextManager.Get("BanDuration")) { Padding = Vector4.Zero }; + new GUITextBlock(new RectTransform(new Vector2(1f, 0.0f), labelContainer.RectTransform), TextManager.Get("BanDuration"), font: GUI.SubHeadingFont) { Padding = Vector4.Zero }; var buttonContent = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), labelContainer.RectTransform), isHorizontal: true); permaBanTickBox = new GUITickBox(new RectTransform(new Vector2(0.4f, 0.15f), buttonContent.RectTransform), TextManager.Get("BanPermanent")) { @@ -3455,7 +3532,10 @@ namespace Barotrauma.Networking errorLines.Add("Campaign ID: " + campaign.CampaignID); errorLines.Add("Campaign save ID: " + campaign.LastSaveID + "(pending: " + campaign.PendingSaveID + ")"); } - errorLines.Add("Mission: " + (GameMain.GameSession?.Mission?.Prefab.Identifier ?? "none")); + foreach (Mission mission in GameMain.GameSession.Missions) + { + errorLines.Add("Mission: " + mission.Prefab.Identifier); + } } if (GameMain.GameSession?.Submarine != null) { 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 b31e4f6e1..e0f4462ae 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs @@ -193,7 +193,7 @@ namespace Barotrauma.Networking continue; } - byte msgLength = msg.ReadByte(); + int msgLength = (int)msg.ReadVariableUInt32(); IServerSerializable entity = Entity.FindEntityByID(entityID) as IServerSerializable; entities.Add(entity); @@ -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 @@ -300,5 +300,15 @@ namespace Barotrauma.Networking MidRoundSyncingDone = false; } + + /// + /// Clears events generated by the current client, used + /// when resynchronizing with the server after a timeout. + /// + public void ClearSelf() + { + ID = 0; + events.Clear(); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/OrderChatMessage.cs index 172b124ca..82ce2130f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/OrderChatMessage.cs @@ -1,6 +1,4 @@ -using System; - -namespace Barotrauma.Networking +namespace Barotrauma.Networking { partial class OrderChatMessage : ChatMessage { @@ -9,26 +7,7 @@ namespace Barotrauma.Networking msg.Write((byte)ClientNetObject.CHAT_MESSAGE); msg.Write(NetStateID); msg.Write((byte)ChatMessageType.Order); - msg.Write((byte)Order.PrefabList.IndexOf(Order.Prefab)); - msg.Write(TargetCharacter == null ? (UInt16)0 : TargetCharacter.ID); - msg.Write(TargetEntity is Entity ? (TargetEntity as Entity).ID : (UInt16)0); - msg.Write((byte)Array.IndexOf(Order.Prefab.Options, OrderOption)); - msg.Write((byte)Order.TargetType); - if (Order.TargetType == Order.OrderTargetType.Position && TargetEntity is OrderTarget orderTarget) - { - msg.Write(true); - msg.Write(orderTarget.Position.X); - msg.Write(orderTarget.Position.Y); - msg.Write(orderTarget.Hull == null ? (UInt16)0 : orderTarget.Hull.ID); - } - else - { - msg.Write(false); - if (Order.TargetType == Order.OrderTargetType.WallSection) - { - msg.Write((byte)(WallSectionIndex ?? Order.WallSectionIndex ?? 0)); - } - } + WriteOrder(msg); } } } 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/ServerInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs index 063201ec6..b3e012d75 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs @@ -378,8 +378,8 @@ namespace Barotrauma.Networking if (maxPlayersElement > NetConfig.MaxPlayers) { - DebugConsole.IsOpen = true; - DebugConsole.NewMessage($"Setting the maximum amount of players to {maxPlayersElement} failed due to exceeding the limit of {NetConfig.MaxPlayers} players per server. Using the maximum of {NetConfig.MaxPlayers} instead.", Color.Red); + /*DebugConsole.IsOpen = true; + DebugConsole.NewMessage($"Setting the maximum amount of players to {maxPlayersElement} failed due to exceeding the limit of {NetConfig.MaxPlayers} players per server. Using the maximum of {NetConfig.MaxPlayers} instead.", Color.Red);*/ maxPlayersElement = NetConfig.MaxPlayers; } @@ -540,5 +540,18 @@ namespace Barotrauma.Networking return element; } + + public override bool Equals(object obj) + { + return obj is ServerInfo other ? Equals(other) : base.Equals(obj); + } + + public bool Equals(ServerInfo other) + { + return + other.OwnerID == OwnerID && + (other.LobbyID == LobbyID || other.LobbyID == 0 || LobbyID == 0) && + ((OwnerID == 0) ? (other.IP == IP && other.Port == Port) : 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/ServerSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs index 313e4a394..7474acbb0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs @@ -500,8 +500,8 @@ namespace Barotrauma.Networking CreateLabeledSlider(roundsTab, "ServerSettingsRespawnInterval", out slider, out sliderLabel); string intervalLabel = sliderLabel.Text; - slider.Step = 0.05f; slider.Range = new Vector2(10.0f, 600.0f); + slider.StepValue = 10.0f; GetPropertyData("RespawnInterval").AssignGUIComponent(slider); slider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs index 658879440..90e5bb010 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs @@ -3,6 +3,7 @@ using Barotrauma.Networking; using RestSharp; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using System.Xml.Linq; @@ -268,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; @@ -285,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) @@ -372,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; @@ -1172,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..f9ca5c84c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs @@ -1,6 +1,8 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Sounds; +using Microsoft.Xna.Framework; using OpenAL; using System; +using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; using System.Threading; @@ -56,7 +58,7 @@ namespace Barotrauma.Networking public bool Disconnected { get; private set; } - public static void Create(string deviceName, UInt16? storedBufferID=null) + public static void Create(string deviceName, UInt16? storedBufferID = null) { if (Instance != null) { @@ -84,7 +86,7 @@ namespace Barotrauma.Networking if (captureDevice == IntPtr.Zero) { - DebugConsole.NewMessage("Alc.CaptureOpenDevice attempt 1 failed: error code " + Alc.GetError(IntPtr.Zero).ToString(),Color.Orange); + DebugConsole.NewMessage("Alc.CaptureOpenDevice attempt 1 failed: error code " + Alc.GetError(IntPtr.Zero).ToString(), Color.Orange); //attempt using a smaller buffer size captureDevice = Alc.CaptureOpenDevice(deviceName, VoipConfig.FREQUENCY, Al.FormatMono16, VoipConfig.BUFFER_SIZE * 2); } @@ -162,6 +164,7 @@ namespace Barotrauma.Networking } } + IntPtr nativeBuffer; short[] uncompressedBuffer = new short[VoipConfig.BUFFER_SIZE]; short[] prevUncompressedBuffer = new short[VoipConfig.BUFFER_SIZE]; bool prevCaptured = true; @@ -171,143 +174,198 @@ namespace Barotrauma.Networking { Array.Copy(uncompressedBuffer, 0, prevUncompressedBuffer, 0, VoipConfig.BUFFER_SIZE); Array.Clear(uncompressedBuffer, 0, VoipConfig.BUFFER_SIZE); - while (capturing && !Disconnected) + nativeBuffer = Marshal.AllocHGlobal(VoipConfig.BUFFER_SIZE * 2); + try { - int alcError; - - if (CanDetectDisconnect) + while (capturing) { - Alc.GetInteger(captureDevice, Alc.EnumConnected, out int isConnected); + int alcError; + + if (CanDetectDisconnect) + { + Alc.GetInteger(captureDevice, Alc.EnumConnected, out int isConnected); + alcError = Alc.GetError(captureDevice); + if (alcError != Alc.NoError) + { + throw new Exception("Failed to determine if capture device is connected: " + alcError.ToString()); + } + + if (isConnected == 0) + { + DebugConsole.ThrowError("Capture device has been disconnected. You can select another available device in the settings."); + Disconnected = true; + break; + } + } + + FillBuffer(); + alcError = Alc.GetError(captureDevice); if (alcError != Alc.NoError) { - throw new Exception("Failed to determine if capture device is connected: " + alcError.ToString()); + throw new Exception("Failed to capture samples: " + alcError.ToString()); } - if (isConnected == 0) + double maxAmplitude = 0.0f; + for (int i = 0; i < VoipConfig.BUFFER_SIZE; i++) { - DebugConsole.ThrowError("Capture device has been disconnected. You can select another available device in the settings."); - Disconnected = true; - break; + uncompressedBuffer[i] = (short)MathHelper.Clamp((uncompressedBuffer[i] * Gain), -short.MaxValue, short.MaxValue); + double sampleVal = uncompressedBuffer[i] / (double)short.MaxValue; + maxAmplitude = Math.Max(maxAmplitude, Math.Abs(sampleVal)); } - } + double dB = Math.Min(20 * Math.Log10(maxAmplitude), 0.0); - Alc.GetInteger(captureDevice, Alc.EnumCaptureSamples, out int sampleCount); + LastdB = dB; + LastAmplitude = maxAmplitude; - alcError = Alc.GetError(captureDevice); - if (alcError != Alc.NoError) - { - throw new Exception("Failed to determine sample count: " + alcError.ToString()); - } - - if (sampleCount < VoipConfig.BUFFER_SIZE) - { - int sleepMs = (VoipConfig.BUFFER_SIZE - sampleCount) * 800 / VoipConfig.FREQUENCY; - if (sleepMs < 5) sleepMs = 5; - Thread.Sleep(sleepMs); - continue; - } - - GCHandle handle = GCHandle.Alloc(uncompressedBuffer, GCHandleType.Pinned); - try - { - Alc.CaptureSamples(captureDevice, handle.AddrOfPinnedObject(), VoipConfig.BUFFER_SIZE); - } - finally - { - handle.Free(); - } - - alcError = Alc.GetError(captureDevice); - if (alcError != Alc.NoError) - { - throw new Exception("Failed to capture samples: " + alcError.ToString()); - } - - double maxAmplitude = 0.0f; - for (int i = 0; i < VoipConfig.BUFFER_SIZE; i++) - { - uncompressedBuffer[i] = (short)MathHelper.Clamp((uncompressedBuffer[i] * Gain), -short.MaxValue, short.MaxValue); - double sampleVal = uncompressedBuffer[i] / (double)short.MaxValue; - maxAmplitude = Math.Max(maxAmplitude, Math.Abs(sampleVal)); - } - double dB = Math.Min(20 * Math.Log10(maxAmplitude), 0.0); - - LastdB = dB; - LastAmplitude = maxAmplitude; - - bool allowEnqueue = false; - if (GameMain.WindowActive) - { - ForceLocal = captureTimer > 0 ? ForceLocal : false; - bool pttDown = false; - if ((PlayerInput.KeyDown(InputType.Voice) || PlayerInput.KeyDown(InputType.LocalVoice)) && - GUI.KeyboardDispatcher.Subscriber == null) + bool allowEnqueue = overrideSound != null; + if (GameMain.WindowActive) { - pttDown = true; - if (PlayerInput.KeyDown(InputType.LocalVoice)) + ForceLocal = captureTimer > 0 ? ForceLocal : GameMain.Config.UseLocalVoiceByDefault; + bool pttDown = false; + if ((PlayerInput.KeyDown(InputType.Voice) || PlayerInput.KeyDown(InputType.LocalVoice)) && + GUI.KeyboardDispatcher.Subscriber == null) { - ForceLocal = true; + pttDown = true; + if (PlayerInput.KeyDown(InputType.LocalVoice)) + { + ForceLocal = true; + } + else + { + ForceLocal = false; + } } - else + if (GameMain.Config.VoiceSetting == GameSettings.VoiceMode.Activity) { - ForceLocal = false; + if (dB > GameMain.Config.NoiseGateThreshold) + { + allowEnqueue = true; + } + } + else if (GameMain.Config.VoiceSetting == GameSettings.VoiceMode.PushToTalk) + { + if (pttDown) + { + allowEnqueue = true; + } } } - if (GameMain.Config.VoiceSetting == GameSettings.VoiceMode.Activity) + + if (allowEnqueue || captureTimer > 0) { - if (dB > GameMain.Config.NoiseGateThreshold) + LastEnqueueAudio = DateTime.Now; + if (GameMain.Client?.Character != null) { - allowEnqueue = true; + var messageType = !ForceLocal && ChatMessage.CanUseRadio(GameMain.Client.Character, out _) ? ChatMessageType.Radio : ChatMessageType.Default; + GameMain.Client.Character.ShowSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]); } - } - else if (GameMain.Config.VoiceSetting == GameSettings.VoiceMode.PushToTalk) - { - if (pttDown) + //encode audio and enqueue it + lock (buffers) { - allowEnqueue = true; + if (!prevCaptured) //enqueue the previous buffer if not sent to avoid cutoff + { + int compressedCountPrev = VoipConfig.Encoder.Encode(prevUncompressedBuffer, 0, VoipConfig.BUFFER_SIZE, BufferToQueue, 0, VoipConfig.MAX_COMPRESSED_SIZE); + EnqueueBuffer(compressedCountPrev); + } + int compressedCount = VoipConfig.Encoder.Encode(uncompressedBuffer, 0, VoipConfig.BUFFER_SIZE, BufferToQueue, 0, VoipConfig.MAX_COMPRESSED_SIZE); + EnqueueBuffer(compressedCount); + } + captureTimer -= (VoipConfig.BUFFER_SIZE * 1000) / VoipConfig.FREQUENCY; + if (allowEnqueue) + { + captureTimer = GameMain.Config.VoiceChatCutoffPrevention; + } + prevCaptured = true; + } + else + { + captureTimer = 0; + prevCaptured = false; + //enqueue silence + lock (buffers) + { + EnqueueBuffer(0); } } } + } + catch (Exception e) + { + DebugConsole.ThrowError($"VoipCapture threw an exception. Disabling capture...", e); + capturing = false; + } + finally + { + Marshal.FreeHGlobal(nativeBuffer); + } + } - if (allowEnqueue || captureTimer > 0) + private Sound overrideSound; + private int overridePos; + private short[] overrideBuf = new short[VoipConfig.BUFFER_SIZE]; + + private void FillBuffer() + { + if (overrideSound != null) + { + int totalSampleCount = 0; + while (totalSampleCount < VoipConfig.BUFFER_SIZE) { - LastEnqueueAudio = DateTime.Now; - if (GameMain.Client?.Character != null) + int sampleCount = overrideSound.FillStreamBuffer(overridePos, overrideBuf); + overridePos += sampleCount * 2; + Array.Copy(overrideBuf, 0, uncompressedBuffer, totalSampleCount, sampleCount); + totalSampleCount += sampleCount; + + if (sampleCount == 0) { - var messageType = !ForceLocal && ChatMessage.CanUseRadio(GameMain.Client.Character, out _) ? ChatMessageType.Radio : ChatMessageType.Default; - GameMain.Client.Character.ShowSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]); + overridePos = 0; } - //encode audio and enqueue it - lock (buffers) + } + int sleepMs = VoipConfig.BUFFER_SIZE * 800 / VoipConfig.FREQUENCY; + Thread.Sleep(sleepMs - 1); + } + else + { + int sampleCount = 0; + + while (sampleCount < VoipConfig.BUFFER_SIZE) + { + Alc.GetInteger(captureDevice, Alc.EnumCaptureSamples, out sampleCount); + + int alcError = Alc.GetError(captureDevice); + if (alcError != Alc.NoError) { - if (!prevCaptured) //enqueue the previous buffer if not sent to avoid cutoff + throw new Exception("Failed to determine sample count: " + alcError.ToString()); + } + + if (sampleCount < VoipConfig.BUFFER_SIZE) + { + int sleepMs = (VoipConfig.BUFFER_SIZE - sampleCount) * 800 / VoipConfig.FREQUENCY; + if (sleepMs >= 1) { - int compressedCountPrev = VoipConfig.Encoder.Encode(prevUncompressedBuffer, 0, VoipConfig.BUFFER_SIZE, BufferToQueue, 0, VoipConfig.MAX_COMPRESSED_SIZE); - EnqueueBuffer(compressedCountPrev); + Thread.Sleep(sleepMs); } - int compressedCount = VoipConfig.Encoder.Encode(uncompressedBuffer, 0, VoipConfig.BUFFER_SIZE, BufferToQueue, 0, VoipConfig.MAX_COMPRESSED_SIZE); - EnqueueBuffer(compressedCount); - } - captureTimer -= (VoipConfig.BUFFER_SIZE * 1000) / VoipConfig.FREQUENCY; - if (allowEnqueue) - { - captureTimer = GameMain.Config.VoiceChatCutoffPrevention; - } - prevCaptured = true; - } - else - { - captureTimer = 0; - prevCaptured = false; - //enqueue silence - lock (buffers) - { - EnqueueBuffer(0); } + + if (!capturing) { return; } } - Thread.Sleep(10); + Alc.CaptureSamples(captureDevice, nativeBuffer, VoipConfig.BUFFER_SIZE); + Marshal.Copy(nativeBuffer, uncompressedBuffer, 0, uncompressedBuffer.Length); + } + } + + public void SetOverrideSound(string fileName) + { + overrideSound?.Dispose(); + if (string.IsNullOrEmpty(fileName)) + { + overrideSound = null; + } + else + { + overrideSound = GameMain.SoundManager.LoadSound(fileName, true); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs index b648cde9c..0d360bd80 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs @@ -94,6 +94,7 @@ namespace Barotrauma.Networking DebugConsole.Log("Recreating voipsound " + queueId); client.VoipSound = new VoipSound(client.Name, GameMain.SoundManager, client.VoipQueue); } + GameMain.SoundManager.ForceStreamUpdate(); if (client.Character != null && !client.Character.IsDead && !client.Character.Removed && client.Character.SpeechImpediment <= 100.0f) { @@ -101,8 +102,8 @@ 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; - if (client.VoipSound.UseRadioFilter) + client.VoipSound.UseRadioFilter = messageType == ChatMessageType.Radio && !GameMain.Config.DisableVoiceChatFilters; + if (messageType == ChatMessageType.Radio) { client.VoipSound.SetRange(radio.Range * 0.8f, radio.Range); } @@ -110,7 +111,7 @@ namespace Barotrauma.Networking { client.VoipSound.SetRange(ChatMessage.SpeakRange * 0.4f, ChatMessage.SpeakRange); } - if (!client.VoipSound.UseRadioFilter && Character.Controlled != null) + if (messageType != ChatMessageType.Radio && 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/Networking/Voip/VoipConfig.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipConfig.cs index ac32f0345..bd907694a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipConfig.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipConfig.cs @@ -12,8 +12,8 @@ namespace Barotrauma.Networking { public static bool Ready = false; - public const int FREQUENCY = 48000; //not amazing, but not bad audio quality - public const int BUFFER_SIZE = 2880; //60ms window, the max Opus seems to support + public const int FREQUENCY = 48000; + public const int BUFFER_SIZE = 960; //20ms window public static OpusEncoder Encoder { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs index c33caf404..959915944 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs @@ -240,7 +240,14 @@ namespace Barotrauma.Particles { animState += deltaTime; int frameCount = ((SpriteSheet)prefab.Sprites[spriteIndex]).FrameCount; - animFrame = (int)Math.Min(Math.Floor(animState / prefab.AnimDuration * frameCount), frameCount - 1); + if (prefab.LoopAnim) + { + animFrame = (int)(Math.Floor(animState / prefab.AnimDuration * frameCount) % frameCount); + } + else + { + animFrame = (int)Math.Min(Math.Floor(animState / prefab.AnimDuration * frameCount), frameCount - 1); + } } lifeTime -= deltaTime; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Program.cs b/Barotrauma/BarotraumaClient/ClientSource/Program.cs index 368eb8951..14c1b3a6e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Program.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Program.cs @@ -218,9 +218,12 @@ namespace Barotrauma sb.AppendLine("Target site: " + exception.TargetSite.ToString()); } - sb.AppendLine("Stack trace: "); - sb.AppendLine(exception.StackTrace.CleanupStackTrace()); - sb.AppendLine("\n"); + if (exception.StackTrace != null) + { + sb.AppendLine("Stack trace: "); + sb.AppendLine(exception.StackTrace.CleanupStackTrace()); + sb.AppendLine("\n"); + } if (exception.InnerException != null) { @@ -229,8 +232,11 @@ namespace Barotrauma { sb.AppendLine("Target site: " + exception.InnerException.TargetSite.ToString()); } - sb.AppendLine("Stack trace: "); - sb.AppendLine(exception.InnerException.StackTrace.CleanupStackTrace()); + if (exception.InnerException.StackTrace != null) + { + sb.AppendLine("Stack trace: "); + sb.AppendLine(exception.InnerException.StackTrace.CleanupStackTrace()); + } } sb.AppendLine("Last debug messages:"); 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..d8dc14228 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; } } @@ -394,6 +394,42 @@ namespace Barotrauma var difficultyLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), textContent.RectTransform), TextManager.Get("LevelDifficulty"), font: GUI.SubHeadingFont, textAlignment: Alignment.CenterLeft); new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), difficultyLabel.RectTransform), ((int)connection.LevelData.Difficulty) + " %", textAlignment: Alignment.CenterRight); + + if (connection.LevelData.HasBeaconStation) + { + var beaconStationContent = new GUILayoutGroup(new RectTransform(biomeLabel.RectTransform.NonScaledSize, textContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + string style = connection.LevelData.IsBeaconActive ? "BeaconStationActive" : "BeaconStationInactive"; + var icon = new GUIImage(new RectTransform(new Point((int)(beaconStationContent.Rect.Height * 1.2f)), beaconStationContent.RectTransform), + style, scaleToFit: true) + { + Color = MapGenerationParams.Instance.IndicatorColor, + HoverColor = Color.Lerp(MapGenerationParams.Instance.IndicatorColor, Color.White, 0.5f), + ToolTip = TextManager.Get(connection.LevelData.IsBeaconActive ? "BeaconStationActiveTooltip" : "BeaconStationInactiveTooltip") + }; + new GUITextBlock(new RectTransform(Vector2.One, beaconStationContent.RectTransform), + TextManager.Get("submarinetype.beaconstation"), font: GUI.SubHeadingFont, textAlignment: Alignment.CenterLeft) + { + Padding = Vector4.Zero, + ToolTip = icon.ToolTip + }; + } + if (connection.LevelData.HasHuntingGrounds) + { + var huntingGroundsContent = new GUILayoutGroup(new RectTransform(biomeLabel.RectTransform.NonScaledSize, textContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + var icon = new GUIImage(new RectTransform(new Point((int)(huntingGroundsContent.Rect.Height * 1.5f)), huntingGroundsContent.RectTransform), + "HuntingGrounds", scaleToFit: true) + { + Color = MapGenerationParams.Instance.IndicatorColor, + HoverColor = Color.Lerp(MapGenerationParams.Instance.IndicatorColor, Color.White, 0.5f), + ToolTip = TextManager.Get("HuntingGroundsTooltip") + }; + new GUITextBlock(new RectTransform(Vector2.One, huntingGroundsContent.RectTransform), + TextManager.Get("missionname.huntinggrounds"), font: GUI.SubHeadingFont, textAlignment: Alignment.CenterLeft) + { + Padding = Vector4.Zero, + ToolTip = icon.ToolTip + }; + } } missionList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.4f), content.RectTransform)) @@ -439,8 +475,9 @@ namespace Barotrauma }; missionName.Padding = new Vector4(missionName.Padding.X + icon.Rect.Width * 1.5f, missionName.Padding.Y, missionName.Padding.Z, missionName.Padding.W); } + string rewardText = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", mission.Reward)); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), - TextManager.GetWithVariable("missionreward", "[reward]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", mission.Reward)), wrap: true); + TextManager.GetWithVariable("missionreward", "[reward]", rewardText), wrap: true); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), mission.Description, wrap: true); } missionPanel.RectTransform.MinSize = new Point(0, (int)(missionTextContent.Children.Sum(c => c.Rect.Height) / missionTextContent.RectTransform.RelativeSize.Y) + GUI.IntScale(20)); @@ -491,7 +528,25 @@ namespace Barotrauma StartButton = new GUIButton(new RectTransform(new Vector2(0.5f, 0.1f), content.RectTransform), TextManager.Get("StartCampaignButton"), style: "GUIButtonLarge") { - OnClicked = (GUIButton btn, object obj) => { StartRound?.Invoke(); return true; }, + OnClicked = (GUIButton btn, object obj) => + { + if (missionList.Content.Children.Any(c => c.UserData is Mission) && !(missionList.SelectedData is Mission)) + { + var noMissionVerification = new GUIMessageBox(string.Empty, TextManager.Get("nomissionprompt"), new string[] { TextManager.Get("yes"), TextManager.Get("no") }); + noMissionVerification.Buttons[0].OnClicked = (btn, userdata) => + { + StartRound?.Invoke(); + noMissionVerification.Close(); + return true; + }; + noMissionVerification.Buttons[1].OnClicked = noMissionVerification.Close; + } + else + { + StartRound?.Invoke(); + } + return true; + }, Enabled = true, Visible = Campaign.AllowedToEndRound() }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index eac9ab768..1a9426868 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -766,7 +766,7 @@ namespace Barotrauma.CharacterEditor if (editLimbs && !spriteSheetRect.Contains(PlayerInput.MousePosition) && MathUtils.RectangleContainsPoint(GetLimbPhysicRect(limb), PlayerInput.MousePosition)) { return CursorState.Hand; } // spritesheet - if (GetLimbSpritesheetRect(limb).Contains(PlayerInput.MousePosition)) { return CursorState.Hand; } + if (showSpritesheet && GetLimbSpritesheetRect(limb).Contains(PlayerInput.MousePosition)) { return CursorState.Hand; } } return CursorState.Default; } @@ -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..4563b2ff6 --- /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() + { + string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + if (!Directory.Exists(home)) { return; } + + FileSelection.OnFileSelected = file => + { + Vector2 pos = Screen.Selected.Cam.ScreenToWorld(PlayerInput.MousePosition); + 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..24b162038 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs @@ -17,8 +17,6 @@ namespace Barotrauma { private GUIFrame GuiFrame = null!; - private GUIListBox? contextMenu; - public override Camera Cam { get; } public static string? DrawnTooltip { get; set; } @@ -399,7 +397,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 +523,7 @@ namespace Barotrauma public override void Select() { + GUI.PreventPauseMenuToggle = false; projectName = TextManager.Get("EventEditor.Unnamed"); base.Select(); } @@ -537,7 +536,6 @@ namespace Barotrauma public override void AddToGUIUpdateList() { GuiFrame.AddToGUIUpdateList(); - contextMenu?.AddToGUIUpdateList(); } private XElement? ExportXML() @@ -596,7 +594,7 @@ namespace Barotrauma foreach (var (node, text, end) in options) { XElement optionElement = new XElement("Option"); - optionElement.Add(new XAttribute("text", text)); + optionElement.Add(new XAttribute("text", text ?? "")); if (end) { optionElement.Add(new XAttribute("endconversation", true)); } if (node is EventNode eventNode) @@ -673,82 +671,37 @@ namespace Barotrauma private void CreateContextMenu(EditorNode node, NodeConnection? connection = null) { - contextMenu = new GUIListBox(new RectTransform(new Vector2(0.1f, 0.1f), GUI.Canvas) { ScreenSpaceOffset = PlayerInput.MousePosition.ToPoint() }, style: "GUIToolTip") { Padding = new Vector4(5) }; + if (GUIContextMenu.CurrentContextMenu != null) { return; } - new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), - TextManager.Get("EventEditor.Edit"), font: GUI.SmallFont) { UserData = "edit", Enabled = node is ValueNode || connection?.Type == NodeConnectionType.Value || connection?.Type == NodeConnectionType.Option }; - - new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), - TextManager.Get("EventEditor.MarkEnding"), font: GUI.SmallFont) { UserData = "markend", Enabled = connection != null && connection.Type == NodeConnectionType.Option }; - - new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), - TextManager.Get("EventEditor.RemoveConnection"), font: GUI.SmallFont) { UserData = "remcon", Enabled = connection != null }; - - new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), - TextManager.Get("EventEditor.AddOption"), font: GUI.SmallFont) { UserData = "addoption", Enabled = node.CanAddConnections }; - - new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), - TextManager.Get("EventEditor.RemoveOption"), font: GUI.SmallFont) { UserData = "removeoption", Enabled = connection != null && node.RemovableTypes.Contains(connection.Type) }; - - new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), - TextManager.Get("EventEditor.Delete"), font: GUI.SmallFont) { UserData = "delete", Enabled = true }; - - foreach (var guiComponent in contextMenu.Content.Children) - { - if (guiComponent is GUITextBlock child) + GUIContextMenu.CreateContextMenu( + new ContextMenuOption("EventEditor.Edit", isEnabled: node is ValueNode || connection?.Type == NodeConnectionType.Value || connection?.Type == NodeConnectionType.Option, onSelected: delegate { - if (!child.Enabled) - { - child.TextColor *= 0.5f; - } - } - } - - foreach (GUIComponent c in contextMenu.Content.Children) - { - if (c is GUITextBlock block) + CreateEditMenu(node as ValueNode, connection); + }), + new ContextMenuOption("EventEditor.MarkEnding", isEnabled: connection != null && connection.Type == NodeConnectionType.Option, onSelected: delegate { - block.RectTransform.NonScaledSize = new Point((int) (block.TextSize.X + block.Padding.X * 2), (int) (18 * GUI.Scale)); - } - } + if (connection == null) { return; } - int biggestSize = contextMenu.Content.Children.Max(c => c.Rect.Width + (int) contextMenu.Padding.X * 2); - contextMenu.Content.Children.ForEach(c => c.RectTransform.MinSize = new Point(biggestSize, c.Rect.Height)); - contextMenu.RectTransform.NonScaledSize = new Point(biggestSize, (int) (contextMenu.Content.Children.Sum(c => c.Rect.Height) + (contextMenu.Padding.X * 2))); - - contextMenu.OnSelected = (component, obj) => - { - if (!component.Enabled) { return false; } - - switch (obj as string) + connection.EndConversation = !connection.EndConversation; + }), + new ContextMenuOption("EventEditor.RemoveConnection", isEnabled: connection != null, onSelected: delegate { - case "edit": - CreateEditMenu(node as ValueNode, connection); - break; - case "markend" when connection != null: - connection.EndConversation = !connection.EndConversation; - break; - case "remcon" when connection != null: - connection.ClearConnections(); - connection.OverrideValue = null; - connection.OptionText = connection.OptionText; - break; - case "addoption": - node.AddOption(); - break; - case "removeoption": - connection?.Parent.RemoveOption(connection); - break; - case "delete": - nodeList.Remove(node); - node.ClearConnections(); + if (connection == null) { return; } - break; - } - - contextMenu = null; - return true; - }; + connection.ClearConnections(); + connection.OverrideValue = null; + connection.OptionText = connection.OptionText; + }), + new ContextMenuOption("EventEditor.AddOption", isEnabled: node.CanAddConnections, onSelected: node.AddOption), + new ContextMenuOption("EventEditor.RemoveOption", isEnabled: connection != null && node.RemovableTypes.Contains(connection.Type), onSelected: delegate + { + connection?.Parent.RemoveOption(connection); + }), + new ContextMenuOption("EventEditor.Delete", isEnabled: true, onSelected: delegate + { + nodeList.Remove(node); + node.ClearConnections(); + })); } private bool CreateTestSetupMenu() @@ -1141,16 +1094,6 @@ namespace Barotrauma DraggingPosition = Vector2.Zero; } - if (contextMenu != null) - { - Rectangle expandedRect = contextMenu.Rect; - expandedRect.Inflate(20, 20); - if (!expandedRect.Contains(PlayerInput.MousePosition)) - { - contextMenu = null; - } - } - if (PlayerInput.MidButtonHeld()) { Vector2 moveSpeed = PlayerInput.MouseSpeed * (float) deltaTime * 60.0f / Cam.Zoom; 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..ab51ff48f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs @@ -489,7 +489,7 @@ namespace Barotrauma { MinValueFloat = 0, MaxValueFloat = 100, - FloatValue = caveGenerationParams.GetCommonness(selectedParams), + FloatValue = caveGenerationParams.GetCommonness(selectedParams, abyss: false), OnValueChanged = (numberInput) => { caveGenerationParams.OverrideCommonness[selectedParams.Identifier] = numberInput.FloatValue; @@ -514,7 +514,7 @@ namespace Barotrauma } foreach (var caveParam in CaveGenerationParams.CaveParams) { - if (selectedParams != null && caveParam.GetCommonness(selectedParams) <= 0.0f) { continue; } + if (selectedParams != null && caveParam.GetCommonness(selectedParams, abyss: false) <= 0.0f) { continue; } availableIdentifiers.Add(caveParam.Identifier); } availableIdentifiers.Reverse(); @@ -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); @@ -810,6 +811,19 @@ namespace Barotrauma GUI.DrawLine(spriteBatch, new Vector2(0, crushDepthScreen), new Vector2(GameMain.GraphicsWidth, crushDepthScreen), GUI.Style.Red * 0.25f, width: 5); GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, crushDepthScreen), "Crush depth", GUI.Style.Red, backgroundColor: Color.Black); } + + float abyssStartScreen = cam.WorldToScreen(new Vector2(0.0f, Level.Loaded.AbyssArea.Bottom)).Y; + if (abyssStartScreen > 0.0f && abyssStartScreen < GameMain.GraphicsHeight) + { + GUI.DrawLine(spriteBatch, new Vector2(0, abyssStartScreen), new Vector2(GameMain.GraphicsWidth, abyssStartScreen), GUI.Style.Blue * 0.25f, width: 5); + GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, abyssStartScreen), "Abyss start", GUI.Style.Blue, backgroundColor: Color.Black); + } + float abyssEndScreen = cam.WorldToScreen(new Vector2(0.0f, Level.Loaded.AbyssArea.Y)).Y; + if (abyssEndScreen > 0.0f && abyssEndScreen < GameMain.GraphicsHeight) + { + GUI.DrawLine(spriteBatch, new Vector2(0, abyssEndScreen), new Vector2(GameMain.GraphicsWidth, abyssEndScreen), GUI.Style.Blue * 0.25f, width: 5); + GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, abyssEndScreen), "Abyss end", GUI.Style.Blue, backgroundColor: Color.Black); + } } GUI.Draw(Cam, spriteBatch); spriteBatch.End(); @@ -817,6 +831,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 +907,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..a89858069 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); @@ -709,7 +709,7 @@ namespace Barotrauma var gamesession = new GameSession( selectedSub, GameModePreset.DevSandbox, - missionPrefab: null); + missionPrefabs: null); //(gamesession.GameMode as SinglePlayerCampaign).GenerateMap(ToolBox.RandomSeed(8)); gamesession.StartRound(fixedSeed ? "abcd" : ToolBox.RandomSeed(8), difficulty: 40); GameMain.GameScreen.Select(); @@ -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 14aa68a2b..c3582ebc4 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; @@ -927,6 +933,7 @@ namespace Barotrauma TextManager.Get("MissionType." + missionType.ToString())) { UserData = (int)missionType, + ToolTip = TextManager.Get("MissionTypeDescription." + missionType.ToString(), returnNull: true), OnSelected = (tickbox) => { int missionTypeOr = tickbox.Selected ? (int)tickbox.UserData : (int)MissionType.None; @@ -1229,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); @@ -1292,7 +1298,7 @@ namespace Barotrauma public void CreatePlayerFrame(GUIComponent parent) { UpdatePlayerFrame( - playerInfoContainer.Children?.First().UserData as CharacterInfo, + Character.Controlled?.Info ?? playerInfoContainer.Children?.First().UserData as CharacterInfo, allowEditing: campaignCharacterInfo == null, parent: parent); } @@ -1372,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) @@ -1487,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() @@ -1748,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; @@ -1818,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; } @@ -2103,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) @@ -2982,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) @@ -3000,6 +3110,7 @@ namespace Barotrauma HighlightedModeIndex = modeIndex; RefreshGameModeContent(); + RefreshEnabledElements(); } private void RefreshMissionTypes() @@ -3046,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 0f4c8c0b8..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))); } }; @@ -696,6 +697,7 @@ namespace Barotrauma private void ReadServerMemFromFile(string file, ref List servers) { if (servers == null) { servers = new List(); } + servers.Clear(); if (!File.Exists(file)) { return; } @@ -716,11 +718,21 @@ namespace Barotrauma return; } + bool saveCleanup = false; foreach (XElement element in doc.Root.Elements()) { if (element.Name != "ServerInfo") { continue; } - servers.Add(ServerInfo.FromXElement(element)); + var info = ServerInfo.FromXElement(element); + if (!servers.Any(s => s.Equals(info))) + { + servers.Add(info); + } + else + { + saveCleanup = true; + } } + if (saveCleanup) { WriteServerMemToFile(file, servers); } } private void WriteServerMemToFile(string file, List servers) @@ -948,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(); @@ -964,6 +989,8 @@ namespace Barotrauma { base.Deselect(); + GameMain.Config.SaveNewPlayerConfig(); + pendingWorkshopDownloads?.Clear(); workshopDownloadsFrame = null; } @@ -1072,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; @@ -1083,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)) @@ -1259,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); } @@ -2266,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 8e4a46c49..29f734718 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs @@ -351,28 +351,48 @@ namespace Barotrauma void LoadSprites(XElement element) { - element.Elements("sprite").ForEach(s => CreateSprite(s)); - element.Elements("Sprite").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("/")) @@ -386,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 62a8c6b3c..dca0fe9f0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -64,6 +64,7 @@ namespace Barotrauma public GUIComponent TopPanel; private GUIComponent showEntitiesPanel, entityCountPanel; private readonly List showEntitiesTickBoxes = new List(); + private readonly Dictionary hiddenSubCategories = new Dictionary(); private GUITextBlock subNameLabel; @@ -74,7 +75,7 @@ namespace Barotrauma private string lastFilter; public GUIComponent EntityMenu; private GUITextBox entityFilterBox; - private GUIListBox entityList; + private GUIListBox categorizedEntityList, allEntityList; private GUIButton toggleEntityMenuButton; public GUIButton ToggleEntityMenuButton => toggleEntityMenuButton; @@ -108,6 +109,8 @@ namespace Barotrauma public static readonly object ItemAddMutex = new object(), ItemRemoveMutex = new object(); + public static bool TransparentWiringMode = true; + private static object bulkItemBufferinUse; public static object BulkItemBufferInUse @@ -127,6 +130,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; @@ -160,8 +167,6 @@ namespace Barotrauma private GUIImage previewImage; private GUILayoutGroup previewImageButtonHolder; - private GUIListBox contextMenu; - private const int submarineNameLimit = 30; private GUITextBlock submarineNameCharacterCount; @@ -171,7 +176,9 @@ namespace Barotrauma private Mode mode; private Color backgroundColor = GameSettings.SubEditorBackgroundColor; - + + private Vector2 MeasurePositionStart = Vector2.Zero; + // Prevent the mode from changing private bool lockMode; @@ -214,7 +221,7 @@ namespace Barotrauma { if (buoyancyVol / selectedVol < 1.0f) { - retVal += " (" + TextManager.GetWithVariable("OptimalBallastLevel", "[value]", (buoyancyVol / selectedVol).ToString("0.000")) + ")"; + retVal += " (" + TextManager.GetWithVariable("OptimalBallastLevel", "[value]", (buoyancyVol / selectedVol).ToString("0.0000")) + ")"; } else { @@ -228,7 +235,10 @@ namespace Barotrauma public SubEditorScreen() { - cam = new Camera(); + cam = new Camera + { + MaxZoom = 10f + }; WayPoint.ShowWayPoints = false; WayPoint.ShowSpawnPoints = false; Hull.ShowHulls = false; @@ -503,15 +513,15 @@ namespace Barotrauma //----------------------------------------------- - showEntitiesPanel = new GUIFrame(new RectTransform(new Vector2(0.08f, 0.5f), GUI.Canvas) + showEntitiesPanel = new GUIFrame(new RectTransform(new Vector2(0.1f, 0.5f), GUI.Canvas) { - MinSize = new Point(170, 0) + MinSize = new Point(190, 0) }) { Visible = false }; - GUILayoutGroup paddedShowEntitiesPanel = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), showEntitiesPanel.RectTransform, Anchor.Center)) + GUILayoutGroup paddedShowEntitiesPanel = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.98f), showEntitiesPanel.RectTransform, Anchor.Center)) { Stretch = true }; @@ -533,7 +543,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; } } @@ -559,6 +568,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", @@ -589,15 +604,38 @@ namespace Barotrauma Selected = Gap.ShowGaps, OnSelected = (GUITickBox obj) => { Gap.ShowGaps = obj.Selected; return true; }, }; - new GUITickBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedShowEntitiesPanel.RectTransform), TextManager.Get("mapentitycategory.thalamus")) - { - UserData = "thalamus", - Selected = ShowThalamus, - OnSelected = (GUITickBox obj) => { ShowThalamus = obj.Selected; return true; }, - }; - showEntitiesTickBoxes.AddRange(paddedShowEntitiesPanel.Children.Select(c => c as GUITickBox)); + var subcategoryHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedShowEntitiesPanel.RectTransform), TextManager.Get("subcategories"), font: GUI.SubHeadingFont); + subcategoryHeader.RectTransform.MinSize = new Point(0, (int)(subcategoryHeader.Rect.Height * 1.5f)); + + var subcategoryList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedShowEntitiesPanel.RectTransform) { MinSize = new Point(0, showEntitiesPanel.Rect.Height / 3) }); + List availableSubcategories = new List(); + foreach (var prefab in MapEntityPrefab.List) + { + if (!string.IsNullOrEmpty(prefab.Subcategory) && !availableSubcategories.Contains(prefab.Subcategory)) + { + availableSubcategories.Add(prefab.Subcategory); + } + } + foreach (string subcategory in availableSubcategories) + { + var tb = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.1f), subcategoryList.Content.RectTransform), + TextManager.Get("subcategory." + subcategory, returnNull: true) ?? subcategory, font: GUI.SmallFont) + { + UserData = subcategory, + Selected = !IsSubcategoryHidden(subcategory), + OnSelected = (GUITickBox obj) => { hiddenSubCategories[(string)obj.UserData] = !obj.Selected; return true; }, + }; + if (tb.TextBlock.TextSize.X > tb.TextBlock.Rect.Width * 1.25f) + { + tb.ToolTip = tb.Text; + tb.Text = ToolBox.LimitString(tb.Text, tb.Font, (int)(tb.TextBlock.Rect.Width * 1.25f)); + } + } + + GUITextBlock.AutoScaleAndNormalize(subcategoryList.Content.Children.Where(c => c is GUITickBox).Select(c => ((GUITickBox)c).TextBlock)); + showEntitiesPanel.RectTransform.NonScaledSize = new Point( (int)(paddedShowEntitiesPanel.RectTransform.Children.Max(c => (int)((c.GUIComponent as GUITickBox)?.TextBlock.TextSize.X ?? 0)) / paddedShowEntitiesPanel.RectTransform.RelativeSize.X), @@ -793,13 +831,18 @@ namespace Barotrauma new GUIFrame(new RectTransform(new Vector2(0.8f, 0.01f), paddedTab.RectTransform), style: "HorizontalLine"); - entityList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.9f), paddedTab.RectTransform), useMouseDownToSelect: true) + var entityListContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.9f), paddedTab.RectTransform), style: null); + categorizedEntityList = new GUIListBox(new RectTransform(Vector2.One, entityListContainer.RectTransform), useMouseDownToSelect: true); + allEntityList = new GUIListBox(new RectTransform(Vector2.One, entityListContainer.RectTransform), useMouseDownToSelect: true) { OnSelected = SelectPrefab, UseGridLayout = true, - CheckSelected = MapEntityPrefab.GetSelected + CheckSelected = MapEntityPrefab.GetSelected, + Visible = false }; - + + paddedTab.Recalculate(); + screenResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); } @@ -845,143 +888,230 @@ namespace Barotrauma private void UpdateEntityList() { - entityList.Content.ClearChildren(); + categorizedEntityList.Content.ClearChildren(); + allEntityList.Content.ClearChildren(); - int entitiesPerRow = (int)Math.Ceiling(entityList.Content.Rect.Width / Math.Max(125 * GUI.Scale, 60)); + int maxTextWidth = (int)(GUI.SubHeadingFont.MeasureString(TextManager.Get("mapentitycategory.misc")).X + GUI.IntScale(50)); + Dictionary> entityLists = new Dictionary>(); + Dictionary categoryKeys = new Dictionary(); + + foreach (MapEntityCategory category in Enum.GetValues(typeof(MapEntityCategory))) + { + foreach (MapEntityPrefab ep in MapEntityPrefab.List) + { + if (!ep.Category.HasFlag(category)) { continue; } + + if (!entityLists.ContainsKey(category + ep.Subcategory)) + { + entityLists[category + ep.Subcategory] = new List(); + } + entityLists[category + ep.Subcategory].Add(ep); + categoryKeys[category + ep.Subcategory] = category; + string categoryName = TextManager.Get("subcategory." + ep.Subcategory, returnNull: true) ?? ep.Subcategory; + if (categoryName != null) + { + maxTextWidth = (int)Math.Max(maxTextWidth, GUI.SubHeadingFont.MeasureString(categoryName.Replace(' ', '\n')).X + GUI.IntScale(50)); + } + } + } + categorizedEntityList.Content.ClampMouseRectToParent = true; + int entitiesPerRow = (int)Math.Ceiling(categorizedEntityList.Content.Rect.Width / Math.Max(125 * GUI.Scale, 60)); + foreach (string categoryKey in entityLists.Keys) + { + var categoryFrame = new GUIFrame(new RectTransform(Vector2.One, categorizedEntityList.Content.RectTransform), style: null) + { + ClampMouseRectToParent = true, + UserData = categoryKeys[categoryKey] + }; + + new GUIFrame(new RectTransform(Vector2.One, categoryFrame.RectTransform), style: "HorizontalLine"); + + string categoryName = entityLists[categoryKey].First().Subcategory; + categoryName = string.IsNullOrEmpty(categoryName) ? + TextManager.Get("mapentitycategory.misc") : + (TextManager.Get("subcategory." + categoryName, returnNull: true) ?? categoryName); + new GUITextBlock( + new RectTransform(new Point(maxTextWidth, categoryFrame.Rect.Height), categoryFrame.RectTransform, Anchor.TopLeft), + categoryName, + textAlignment: Alignment.TopLeft, + font: GUI.SubHeadingFont, + wrap: true) + { + Padding = new Vector4(GUI.IntScale(10)) + }; + + var entityListInner = new GUIListBox(new RectTransform(new Point(categoryFrame.Rect.Width - maxTextWidth, categoryFrame.Rect.Height), categoryFrame.RectTransform, Anchor.CenterRight), + style: null, + useMouseDownToSelect: true) + { + ScrollBarVisible = false, + AutoHideScrollBar = false, + OnSelected = SelectPrefab, + UseGridLayout = true, + CheckSelected = MapEntityPrefab.GetSelected, + ClampMouseRectToParent = true + }; + entityListInner.ContentBackground.ClampMouseRectToParent = true; + entityListInner.Content.ClampMouseRectToParent = true; + + foreach (MapEntityPrefab ep in entityLists[categoryKey]) + { +#if !DEBUG + if (ep.HideInMenus) { continue; } +#endif + + CreateEntityElement(ep, entitiesPerRow, entityListInner.Content); + } + + entityListInner.UpdateScrollBarSize(); + int contentHeight = (int)(entityListInner.TotalSize + entityListInner.Padding.Y + entityListInner.Padding.W); + categoryFrame.RectTransform.NonScaledSize = new Point(categoryFrame.Rect.Width, contentHeight); + categoryFrame.RectTransform.MinSize = new Point(0, contentHeight); + entityListInner.RectTransform.NonScaledSize = new Point(entityListInner.Rect.Width, contentHeight); + entityListInner.RectTransform.MinSize = new Point(0, contentHeight); + + entityListInner.Content.RectTransform.SortChildren((i1, i2) => + string.Compare(((MapEntityPrefab)i1.GUIComponent.UserData). Name, (i2.GUIComponent.UserData as MapEntityPrefab)?.Name, StringComparison.Ordinal)); + } foreach (MapEntityPrefab ep in MapEntityPrefab.List) { -#if !DEBUG - if (ep.HideInMenus) { continue; } -#endif + CreateEntityElement(ep, entitiesPerRow, allEntityList.Content); + } + } - bool legacy = ep.Category.HasFlag(MapEntityCategory.Legacy); + private void CreateEntityElement(MapEntityPrefab ep, int entitiesPerRow, GUIComponent parent) + { + bool legacy = ep.Category.HasFlag(MapEntityCategory.Legacy); - float relWidth = 1.0f / entitiesPerRow; - GUIFrame frame = new GUIFrame(new RectTransform( - new Vector2(relWidth, relWidth * ((float)entityList.Content.Rect.Width / entityList.Content.Rect.Height)), - entityList.Content.RectTransform) { MinSize = new Point(0, 50) }, - style: "GUITextBox") - { - UserData = ep, - }; - frame.RectTransform.MinSize = new Point(0, frame.Rect.Width); - frame.RectTransform.MaxSize = new Point(int.MaxValue, frame.Rect.Width); + float relWidth = 1.0f / entitiesPerRow; + GUIFrame frame = new GUIFrame(new RectTransform( + new Vector2(relWidth, relWidth * ((float)parent.Rect.Width / parent.Rect.Height)), + parent.RectTransform) + { MinSize = new Point(0, 50) }, + style: "GUITextBox") + { + UserData = ep, + ClampMouseRectToParent = true + }; + frame.RectTransform.MinSize = new Point(0, frame.Rect.Width); + frame.RectTransform.MaxSize = new Point(int.MaxValue, frame.Rect.Width); - string name = legacy ? TextManager.GetWithVariable("legacyitemformat", "[name]", ep.Name) : ep.Name; - frame.ToolTip = string.IsNullOrEmpty(ep.Description) ? name : name + '\n' + ep.Description; + string name = legacy ? TextManager.GetWithVariable("legacyitemformat", "[name]", ep.Name) : ep.Name; + frame.ToolTip = string.IsNullOrEmpty(ep.Description) ? name : name + '\n' + ep.Description; - if (ep.HideInMenus) - { - frame.Color = Color.Red; - name = "[HIDDEN] " + name; - } - - GUILayoutGroup paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.8f), frame.RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) - { - Stretch = true, - RelativeSpacing = 0.03f, - CanBeFocused = false - }; - - Sprite icon = ep.sprite; - Color iconColor = Color.White; - if (ep is ItemPrefab itemPrefab) - { - if (itemPrefab.InventoryIcon != null) - { - icon = itemPrefab.InventoryIcon; - iconColor = itemPrefab.InventoryIconColor; - } - else - { - iconColor = itemPrefab.SpriteColor; - } - } - GUIImage img = null; - if (ep.sprite != null) - { - img = new GUIImage(new RectTransform(new Vector2(1.0f, 0.8f), - paddedFrame.RectTransform, Anchor.TopCenter), icon) - { - CanBeFocused = false, - LoadAsynchronously = true, - Color = legacy ? iconColor * 0.6f : iconColor - }; - } - - if (ep is ItemAssemblyPrefab itemAssemblyPrefab) - { - new GUICustomComponent(new RectTransform(new Vector2(1.0f, 0.75f), - paddedFrame.RectTransform, Anchor.TopCenter), onDraw: (sb, customComponent) => - { - if (GUIImage.LoadingTextures) { return; } - itemAssemblyPrefab.DrawIcon(sb, customComponent); - }) - { - HideElementsOutsideFrame = true, - ToolTip = frame.RawToolTip - }; - } - - GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform, Anchor.BottomCenter), - text: name, textAlignment: Alignment.Center, font: GUI.SmallFont) - { - CanBeFocused = false - }; - if (legacy) textBlock.TextColor *= 0.6f; - textBlock.Text = ToolBox.LimitString(textBlock.Text, textBlock.Font, textBlock.Rect.Width); - - if (ep.Category == MapEntityCategory.ItemAssembly) - { - var deleteButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform, Anchor.BottomCenter) { MinSize = new Point(0, 20) }, - TextManager.Get("Delete"), style: "GUIButtonSmall") - { - UserData = ep, - OnClicked = (btn, userData) => - { - ItemAssemblyPrefab assemblyPrefab = (ItemAssemblyPrefab) userData; - if (assemblyPrefab != null) - { - var msgBox = new GUIMessageBox( - TextManager.Get("DeleteDialogLabel"), - TextManager.GetWithVariable("DeleteDialogQuestion", "[file]", assemblyPrefab.Name), - new[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }); - msgBox.Buttons[0].OnClicked += (deleteBtn, userData2) => - { - try - { - assemblyPrefab.Delete(); - UpdateEntityList(); - OpenEntityMenu(MapEntityCategory.ItemAssembly); - } - catch (Exception e) - { - DebugConsole.ThrowError(TextManager.GetWithVariable("DeleteFileError", "[file]", assemblyPrefab.Name), e); - } - return true; - }; - msgBox.Buttons[0].OnClicked += msgBox.Close; - msgBox.Buttons[1].OnClicked += msgBox.Close; - } - - return true; - } - }; - } - paddedFrame.Recalculate(); - if (img != null) - { - img.Scale = Math.Min(Math.Min(img.Rect.Width / img.Sprite.size.X, img.Rect.Height / img.Sprite.size.Y), 1.5f); - img.RectTransform.NonScaledSize = new Point((int)(img.Sprite.size.X * img.Scale), img.Rect.Height); - } + if (ep.HideInMenus) + { + frame.Color = Color.Red; + name = "[HIDDEN] " + name; } - entityList.Content.RectTransform.SortChildren((i1, i2) => - string.Compare(((MapEntityPrefab) i1.GUIComponent.UserData). Name, (i2.GUIComponent.UserData as MapEntityPrefab)?.Name, StringComparison.Ordinal)); + GUILayoutGroup paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.8f), frame.RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) + { + Stretch = true, + RelativeSpacing = 0.03f, + CanBeFocused = false + }; + + Sprite icon = ep.sprite; + Color iconColor = Color.White; + if (ep is ItemPrefab itemPrefab) + { + if (itemPrefab.InventoryIcon != null) + { + icon = itemPrefab.InventoryIcon; + iconColor = itemPrefab.InventoryIconColor; + } + else + { + iconColor = itemPrefab.SpriteColor; + } + } + GUIImage img = null; + if (ep.sprite != null) + { + img = new GUIImage(new RectTransform(new Vector2(1.0f, 0.8f), + paddedFrame.RectTransform, Anchor.TopCenter), icon) + { + CanBeFocused = false, + LoadAsynchronously = true, + Color = legacy ? iconColor * 0.6f : iconColor + }; + } + + if (ep is ItemAssemblyPrefab itemAssemblyPrefab) + { + new GUICustomComponent(new RectTransform(new Vector2(1.0f, 0.75f), + paddedFrame.RectTransform, Anchor.TopCenter), onDraw: (sb, customComponent) => + { + if (GUIImage.LoadingTextures) { return; } + itemAssemblyPrefab.DrawIcon(sb, customComponent); + }) + { + HideElementsOutsideFrame = true, + ToolTip = frame.RawToolTip + }; + } + + GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform, Anchor.BottomCenter), + text: name, textAlignment: Alignment.Center, font: GUI.SmallFont) + { + CanBeFocused = false + }; + if (legacy) textBlock.TextColor *= 0.6f; + textBlock.Text = ToolBox.LimitString(textBlock.Text, textBlock.Font, textBlock.Rect.Width); + + if (ep.Category == MapEntityCategory.ItemAssembly) + { + var deleteButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform, Anchor.BottomCenter) { MinSize = new Point(0, 20) }, + TextManager.Get("Delete"), style: "GUIButtonSmall") + { + UserData = ep, + OnClicked = (btn, userData) => + { + ItemAssemblyPrefab assemblyPrefab = (ItemAssemblyPrefab)userData; + if (assemblyPrefab != null) + { + var msgBox = new GUIMessageBox( + TextManager.Get("DeleteDialogLabel"), + TextManager.GetWithVariable("DeleteDialogQuestion", "[file]", assemblyPrefab.Name), + new[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }); + msgBox.Buttons[0].OnClicked += (deleteBtn, userData2) => + { + try + { + assemblyPrefab.Delete(); + UpdateEntityList(); + OpenEntityMenu(MapEntityCategory.ItemAssembly); + } + catch (Exception e) + { + DebugConsole.ThrowError(TextManager.GetWithVariable("DeleteFileError", "[file]", assemblyPrefab.Name), e); + } + return true; + }; + msgBox.Buttons[0].OnClicked += msgBox.Close; + msgBox.Buttons[1].OnClicked += msgBox.Close; + } + + return true; + } + }; + } + paddedFrame.Recalculate(); + if (img != null) + { + img.Scale = Math.Min(Math.Min(img.Rect.Width / img.Sprite.size.X, img.Rect.Height / img.Sprite.size.Y), 1.5f); + img.RectTransform.NonScaledSize = new Point((int)(img.Sprite.size.X * img.Scale), img.Rect.Height); + } } public override void Select() + { + Select(enableAutoSave: true); + } + + public void Select(bool enableAutoSave = true) { base.Select(); @@ -1018,11 +1148,6 @@ namespace Barotrauma new Color(3, 3, 3, 3); UpdateEntityList(); - if (!wasSelectedBefore) - { - OpenEntityMenu(MapEntityCategory.Structure); - wasSelectedBefore = true; - } isAutoSaving = false; if (!wasSelectedBefore) @@ -1048,6 +1173,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) @@ -1062,10 +1191,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); } @@ -1073,11 +1204,13 @@ namespace Barotrauma CreateDummyCharacter(); - if (GameSettings.EnableSubmarineAutoSave) + if (GameSettings.EnableSubmarineAutoSave && enableAutoSave) { CoroutineManager.StartCoroutine(AutoSaveCoroutine(), "SubEditorAutoSave"); } + ImageManager.OnEditorSelected(); + GameAnalyticsManager.SetCustomDimension01("editor"); if (!GameMain.Config.EditorDisclaimerShown) { @@ -1092,7 +1225,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; @@ -1162,7 +1295,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(); } @@ -1170,7 +1316,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 @@ -1215,7 +1361,7 @@ namespace Barotrauma CrossThread.RequestExecutionOnMainThread(() => { - if (AutoSaveInfo?.Root == null) { return; } + if (AutoSaveInfo?.Root == null || Submarine.MainSub?.Info == null) { return; } int saveCount = AutoSaveInfo.Root.Elements().Count(); while (AutoSaveInfo.Root.Elements().Count() > maxAutoSaves) @@ -1412,7 +1558,9 @@ namespace Barotrauma msgBox.Buttons[0].OnClicked = (bt, userdata) => { contentPackage.AddFile(savePath, ContentType.OutpostModule); + Barotrauma.IO.Validation.DevException = true; contentPackage.Save(contentPackage.Path, reload: false); + Barotrauma.IO.Validation.DevException = false; msgBox.Close(); return true; }; @@ -1488,7 +1636,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(); @@ -1516,10 +1664,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); @@ -1622,14 +1772,14 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(0.4f, 1f), subTypeContainer.RectTransform), TextManager.Get("submarinetype")); var subTypeDropdown = new GUIDropDown(new RectTransform(new Vector2(0.6f, 1f), subTypeContainer.RectTransform)); subTypeContainer.RectTransform.MinSize = new Point(0, subTypeContainer.RectTransform.Children.Max(c => c.MinSize.Y)); - subTypeDropdown.AddItem(TextManager.Get("submarinetype.player"), SubmarineType.Player); - subTypeDropdown.AddItem(TextManager.Get("submarinetype.outpostmodule"), SubmarineType.OutpostModule); - subTypeDropdown.AddItem(TextManager.Get("submarinetype.outpost"), SubmarineType.Outpost); - subTypeDropdown.AddItem(TextManager.Get("submarinetype.wreck"), SubmarineType.Wreck); + foreach (SubmarineType subType in Enum.GetValues(typeof(SubmarineType))) + { + subTypeDropdown.AddItem(TextManager.Get("submarinetype."+subType.ToString().ToLowerInvariant()), subType); + } - //--------------------------------------- + //--------------------------------------- - var outpostSettingsContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), leftColumn.RectTransform)) + var outpostSettingsContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), leftColumn.RectTransform)) { IgnoreLayoutGroups = true, CanBeFocused = true, @@ -1680,10 +1830,6 @@ namespace Barotrauma }; outpostModuleGroup.RectTransform.MinSize = new Point(0, outpostModuleGroup.RectTransform.Children.Max(c => c.MinSize.Y)); - - - - // module flags --------------------- var allowAttachGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.1f), outpostSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); @@ -1720,24 +1866,13 @@ namespace Barotrauma }; allowAttachGroup.RectTransform.MinSize = new Point(0, allowAttachGroup.RectTransform.Children.Max(c => c.MinSize.Y)); - - - - - - - - - - - // location types --------------------- var locationTypeGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.1f), outpostSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); 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); @@ -1832,7 +1967,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, @@ -2327,9 +2462,9 @@ namespace Barotrauma new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, loadFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); - var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.25f, 0.5f), loadFrame.RectTransform, Anchor.Center) { MinSize = new Point(350, 500) }); + var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.3f, 0.75f), loadFrame.RectTransform, Anchor.Center) { MinSize = new Point(350, 500) }); - var paddedLoadFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), innerFrame.RectTransform, Anchor.Center)) { Stretch = true, RelativeSpacing = 0.02f }; + var paddedLoadFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), innerFrame.RectTransform, Anchor.Center)) { Stretch = true, RelativeSpacing = 0.01f }; var deleteButtonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.2f), paddedLoadFrame.RectTransform, Anchor.Center)) { @@ -2367,7 +2502,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; @@ -2376,7 +2512,7 @@ namespace Barotrauma { if (prevSub == null || prevSub.Type != sub.Type) { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), subList.Content.RectTransform) { MinSize = new Point(0, 35) }, + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), subList.Content.RectTransform) { MinSize = new Point(0, 35) }, TextManager.Get("SubmarineType." + sub.Type), font: GUI.LargeFont, textAlignment: Alignment.Center, style: "ListBoxElement") { CanBeFocused = false @@ -2384,7 +2520,7 @@ namespace Barotrauma prevSub = sub; } - GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), subList.Content.RectTransform) { MinSize = new Point(0, 30) }, + GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), subList.Content.RectTransform) { MinSize = new Point(0, 30) }, ToolBox.LimitString(sub.Name, GUI.Font, subList.Rect.Width - 80)) { UserData = sub, @@ -2501,6 +2637,8 @@ namespace Barotrauma { OnClicked = LoadSub }; + + controlBtnHolder.RectTransform.MaxSize = new Point(int.MaxValue, controlBtnHolder.Children.First().Rect.Height); } private void FilterSubs(GUIListBox subList, string filter) @@ -2687,9 +2825,9 @@ namespace Barotrauma child.SpriteEffects = entityMenuOpen ? SpriteEffects.None : SpriteEffects.FlipVertically; } - foreach (GUIComponent child in entityList.Content.Children) + foreach (GUIComponent child in categorizedEntityList.Content.Children) { - child.Visible = !entityCategory.HasValue || ((MapEntityPrefab) child.UserData).Category.HasFlag(entityCategory); + child.Visible = !entityCategory.HasValue || (MapEntityCategory)child.UserData == entityCategory; if (child.Visible && dummyCharacter?.SelectedConstruction?.OwnInventory != null) { child.Visible = child.UserData is MapEntityPrefab item && IsItemPrefab(item); @@ -2698,50 +2836,60 @@ namespace Barotrauma if (!string.IsNullOrEmpty(entityFilterBox.Text)) { FilterEntities(entityFilterBox.Text); } - entityList.UpdateScrollBarSize(); - entityList.BarScroll = 0.0f; + categorizedEntityList.UpdateScrollBarSize(); + categorizedEntityList.BarScroll = 0.0f; + categorizedEntityList.Visible = true; + allEntityList.Visible = false; } private void FilterEntities(string filter) { if (string.IsNullOrWhiteSpace(filter)) { - entityList.Content.Children.ForEach(c => + allEntityList.Visible = false; + categorizedEntityList.Visible = true; + + foreach (GUIComponent child in categorizedEntityList.Content.Children) { - c.Visible = !selectedCategory.HasValue || selectedCategory == ((MapEntityPrefab) c.UserData).Category; - if (c.Visible && dummyCharacter?.SelectedConstruction?.OwnInventory != null) + child.Visible = !selectedCategory.HasValue || selectedCategory == (MapEntityCategory)child.UserData; + if (!child.Visible) { return; } + var innerList = child.GetChild(); + foreach (GUIComponent grandChild in innerList.Content.Children) { - c.Visible = c.UserData is MapEntityPrefab item && IsItemPrefab(item); + grandChild.Visible = ((MapEntityPrefab)grandChild.UserData).Name.ToLower().Contains(filter); + if (grandChild.Visible && dummyCharacter?.SelectedConstruction?.OwnInventory != null) + { + grandChild.Visible = grandChild.UserData is MapEntityPrefab item && IsItemPrefab(item); + } } - }); - entityList.UpdateScrollBarSize(); - entityList.BarScroll = 0.0f; - + }; + categorizedEntityList.UpdateScrollBarSize(); + categorizedEntityList.BarScroll = 0.0f; return; } + allEntityList.Visible = true; + categorizedEntityList.Visible = false; filter = filter.ToLower(); - foreach (GUIComponent child in entityList.Content.Children) + foreach (GUIComponent child in allEntityList.Content.Children) { - var textBlock = child.GetChild(); - child.Visible = - (!selectedCategory.HasValue || ((MapEntityPrefab) child.UserData).Category.HasFlag(selectedCategory)) && - ((MapEntityPrefab) child.UserData).Name.ToLower().Contains(filter); - + child.Visible = + (!selectedCategory.HasValue || ((MapEntityPrefab)child.UserData).Category.HasFlag(selectedCategory)) && + ((MapEntityPrefab)child.UserData).Name.ToLower().Contains(filter); ; if (child.Visible && dummyCharacter?.SelectedConstruction?.OwnInventory != null) { child.Visible = child.UserData is MapEntityPrefab item && IsItemPrefab(item); } } - entityList.UpdateScrollBarSize(); - entityList.BarScroll = 0.0f; + allEntityList.UpdateScrollBarSize(); + allEntityList.BarScroll = 0.0f; } private void ClearFilter() { FilterEntities(""); - entityList.UpdateScrollBarSize(); - entityList.BarScroll = 0.0f; + categorizedEntityList.UpdateScrollBarSize(); + categorizedEntityList.BarScroll = 0.0f; entityFilterBox.Text = ""; } @@ -2777,29 +2925,19 @@ 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; } private void CreateContextMenu() { + if (GUIContextMenu.CurrentContextMenu != null) { return; } + List targets = MapEntity.mapEntityList.Any(me => me.IsHighlighted && !MapEntity.SelectedList.Contains(me)) ? MapEntity.mapEntityList.Where(me => me.IsHighlighted).ToList() : new List(MapEntity.SelectedList); - contextMenu = new GUIListBox(new RectTransform(new Vector2(0.1f, 0.1f), GUI.Canvas) - { - ScreenSpaceOffset = PlayerInput.MousePosition.ToPoint() - }, style: "GUIToolTip") - { - Padding = new Vector4(5) - }; - Item target = null; var single = targets.Count == 1 ? targets.Single() : null; @@ -2809,115 +2947,291 @@ namespace Barotrauma var container = item.GetComponent(); if (container == null || container.DrawInventory) { target = item; } } - + // Holding shift brings up special context menu options if (PlayerInput.IsShiftDown()) { - new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), - TextManager.Get("CharacterEditor.EditBackgroundColor"), font: GUI.SmallFont) - { - UserData = "bgcolor" - }; - - new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), - TextManager.Get("editor.selectsame"), font: GUI.SmallFont) - { - UserData = "selectsame", - Enabled = targets.Count > 0 - }; + GUIContextMenu.CreateContextMenu( + new ContextMenuOption("SubEditor.EditBackgroundColor", isEnabled: true, onSelected: CreateBackgroundColorPicker), + new ContextMenuOption("SubEditor.ToggleTransparency", isEnabled: true, onSelected: () => TransparentWiringMode = !TransparentWiringMode), + new ContextMenuOption("SubEditor.ToggleGrid", isEnabled: true, onSelected: () => ShouldDrawGrid = !ShouldDrawGrid), + new ContextMenuOption("SubEditor.PasteAssembly", isEnabled: true, PasteAssembly), + new ContextMenuOption("Editor.SelectSame", isEnabled: targets.Count > 0, onSelected: delegate + { + IEnumerable matching = MapEntity.mapEntityList.Where(e => e.prefab != null && targets.Any(t => t.prefab?.Identifier == e.prefab.Identifier) && !MapEntity.SelectedList.Contains(e)); + MapEntity.SelectedList.AddRange(matching); + }), + new ContextMenuOption("SubEditor.AddImage", isEnabled: true, onSelected: ImageManager.CreateImageWizard), + new ContextMenuOption("SubEditor.ToggleImageEditing", isEnabled: true, onSelected: delegate + { + ImageManager.EditorMode = !ImageManager.EditorMode; + if (!ImageManager.EditorMode) { GameMain.Config.SaveNewPlayerConfig(); } + })); } else { - new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), - TextManager.Get("label.openlabel"), font: GUI.SmallFont) - { - UserData = "open", - Enabled = target != null - }; - - new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), - TextManager.Get("editor.cut"), font: GUI.SmallFont) - { - UserData = "cut", - Enabled = targets.Count > 0 - }; - - new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), - TextManager.Get("editor.copytoclipboard"), font: GUI.SmallFont) - { - UserData = "copy", - Enabled = targets.Count > 0 - }; - - new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), - TextManager.Get("editor.paste"), font: GUI.SmallFont) - { - UserData = "paste", - Enabled = MapEntity.CopiedList.Any(), - }; - - new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), - TextManager.Get("delete"), font: GUI.SmallFont) - { - UserData = "delete", - Enabled = targets.Count > 0 - }; + GUIContextMenu.CreateContextMenu( + new ContextMenuOption("label.openlabel", isEnabled: target != null, onSelected: () => OpenItem(target)), + new ContextMenuOption("editor.cut", isEnabled: targets.Count > 0, onSelected: () => MapEntity.Cut(targets)), + new ContextMenuOption("editor.copytoclipboard", isEnabled: targets.Count > 0, onSelected: () => MapEntity.Copy(targets)), + new ContextMenuOption("editor.paste", isEnabled: MapEntity.CopiedList.Any(), onSelected: () => MapEntity.Paste(cam.ScreenToWorld(PlayerInput.MousePosition))), + new ContextMenuOption("delete", isEnabled: targets.Count > 0, onSelected: delegate + { + StoreCommand(new AddOrDeleteCommand(targets, true)); + foreach (var me in targets) + { + if (!me.Removed) { me.Remove(); } + } + })); + } + } + + private void PasteAssembly() + { + string clipboard = Clipboard.GetText(); + if (string.IsNullOrWhiteSpace(clipboard)) + { + DebugConsole.ThrowError("Unable to paste assembly: Clipboard content is empty."); + return; } - foreach (var guiComponent in contextMenu.Content.Children) + XElement element = null; + + try { - if (guiComponent is GUITextBlock child) + element = XDocument.Parse(clipboard).Root; + } + catch (Exception) { /* ignored */ } + + if (element == null) + { + DebugConsole.ThrowError("Unable to paste assembly: Clipboard content is not valid XML."); + return; + } + + Vector2 pos = cam.ScreenToWorld(PlayerInput.MousePosition); + Submarine sub = Submarine.MainSub; + List entities; + try + { + entities = ItemAssemblyPrefab.PasteEntities(pos, sub, element, selectInstance: true); + } + catch (Exception e) + { + DebugConsole.ThrowError("Unable to paste assembly: Failed to load items.", e); + return; + } + + if (!entities.Any()) { return; } + StoreCommand(new AddOrDeleteCommand(entities, false, handleInventoryBehavior: false)); + } + + 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))) { - if (!child.Enabled) + GUIListBox list = MapEntity.EditingHUD.GetChild(); + if (list != null) { - child.TextColor *= 0.5f; + 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; } } - } - - contextMenu.Content.Children.ForEach(c => - { - if (c is GUITextBlock block) - { - block.RectTransform.NonScaledSize = new Point((int) (block.TextSize.X + block.Padding.X * 2), (int)(18 * GUI.Scale)); - } - }); - int biggestSize = contextMenu.Content.Children.Max(c => c.Rect.Width + (int)contextMenu.Padding.X * 2); - contextMenu.Content.Children.ForEach(c => c.RectTransform.MinSize = new Point(biggestSize, c.Rect.Height)); - contextMenu.RectTransform.NonScaledSize = new Point(biggestSize, (int)(contextMenu.Content.Children.Sum(c => c.Rect.Height) + (contextMenu.Padding.X * 2))); - - contextMenu.OnSelected = (component, obj) => - { - if (!component.Enabled) { return false; } - switch (obj as string) - { - case "bgcolor": - CreateBackgroundColorPicker(); - 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)); - MapEntity.SelectedList.AddRange(matching); - break; - case "copy": - MapEntity.Copy(targets); - break; - case "cut": - MapEntity.Cut(targets); - break; - case "paste": - MapEntity.Paste(cam.ScreenToWorld(contextMenu.Rect.Location.ToVector2())); - break; - case "delete": - StoreCommand(new AddOrDeleteCommand(targets, true)); - targets.ForEach(me => { if (!me.Removed) { me.Remove(); }}); - break; - case "open" when target != null: - OpenItem(target); - break; - } - contextMenu = null; 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}"; } /// @@ -3014,7 +3328,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); @@ -3027,7 +3341,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); @@ -3241,7 +3555,7 @@ namespace Barotrauma case ItemPrefab _: { // Place the item into our hands - DraggedItemPrefab = (MapEntityPrefab) obj; + DraggedItemPrefab = (MapEntityPrefab)obj; SoundPlayer.PlayUISound(GUISoundType.PickItem); break; } @@ -3605,11 +3919,7 @@ namespace Barotrauma wiringToolPanel.AddToGUIUpdateList(); } - if (contextMenu != null) - { - contextMenu.AddToGUIUpdateList(); - } - else if (MapEntity.HighlightedListBox != null) + if (MapEntity.HighlightedListBox != null) { MapEntity.HighlightedListBox.AddToGUIUpdateList(); } @@ -3758,6 +4068,8 @@ namespace Barotrauma /// public override void Update(double deltaTime) { + ImageManager.Update((float) deltaTime); + if (GameMain.GraphicsWidth != screenResolution.X || GameMain.GraphicsHeight != screenResolution.Y) { saveFrame = null; @@ -3769,9 +4081,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) @@ -3860,6 +4171,25 @@ namespace Barotrauma if (GUI.KeyboardDispatcher.Subscriber == null) { + 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 (PlayerInput.KeyHit(Keys.E) && mode == Mode.Default) { if (dummyCharacter != null) @@ -3986,16 +4316,6 @@ namespace Barotrauma GameMain.LightManager?.Update((float)deltaTime); } - if (contextMenu != null) - { - Rectangle expandedRect = contextMenu.Rect; - expandedRect.Inflate(20, 20); - if (!expandedRect.Contains(PlayerInput.MousePosition)) - { - contextMenu = null; - } - } - if (dummyCharacter != null && Entity.FindEntityByID(dummyCharacter.ID) == dummyCharacter) { if (WiringMode) @@ -4021,7 +4341,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); @@ -4033,9 +4353,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(); @@ -4081,12 +4399,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) { @@ -4094,17 +4412,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; @@ -4114,8 +4432,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; @@ -4133,6 +4450,7 @@ namespace Barotrauma else { newItem.Remove(); + slot.ShowBorderHighlight(GUI.Style.Red, 0.1f, 0.4f); } if (!newItem.Removed) @@ -4153,11 +4471,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); @@ -4171,14 +4490,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; @@ -4248,6 +4567,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) { @@ -4289,14 +4621,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(); @@ -4321,10 +4645,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); @@ -4372,8 +4694,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)); @@ -4381,7 +4706,7 @@ namespace Barotrauma } Submarine.DrawBack(spriteBatch, true, e => e is Structure s && - (ShowThalamus || !s.prefab.Category.HasFlag(MapEntityCategory.Thalamus)) && + !IsSubcategoryHidden(e.prefab?.Subcategory) && (e.SpriteDepth >= 0.9f || s.Prefab.BackgroundSprite != null)); Submarine.DrawPaintedColors(spriteBatch, true); spriteBatch.End(); @@ -4402,15 +4727,15 @@ namespace Barotrauma Submarine.DrawBack(spriteBatch, true, e => (!(e is Structure) || e.SpriteDepth < 0.9f) && - (ShowThalamus || !e.prefab.Category.HasFlag(MapEntityCategory.Thalamus))); + !IsSubcategoryHidden(e.prefab?.Subcategory)); spriteBatch.End(); spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, transformMatrix: cam.Transform); - Submarine.DrawDamageable(spriteBatch, null, editing: true, e => ShowThalamus || !(e.prefab?.Category.HasFlag(MapEntityCategory.Thalamus) ?? false)); + Submarine.DrawDamageable(spriteBatch, null, editing: true, e => !IsSubcategoryHidden(e.prefab?.Subcategory)); spriteBatch.End(); spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, transformMatrix: cam.Transform); - Submarine.DrawFront(spriteBatch, editing: true, e => ShowThalamus || !(e.prefab?.Category.HasFlag(MapEntityCategory.Thalamus) ?? false)); + Submarine.DrawFront(spriteBatch, editing: true, e => !IsSubcategoryHidden(e.prefab?.Subcategory)); if (!WiringMode && !IsMouseOnEditorGUI()) { MapEntityPrefab.Selected?.DrawPlacing(spriteBatch, cam); @@ -4418,15 +4743,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); @@ -4438,7 +4765,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; @@ -4476,6 +4803,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(); } @@ -4524,6 +4876,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); @@ -4531,7 +4919,17 @@ namespace Barotrauma stream.Dispose(); } + public bool IsSubcategoryHidden(string subcategory) + { + if (string.IsNullOrEmpty(subcategory) || !hiddenSubCategories.ContainsKey(subcategory)) + { + return false; + } + return hiddenSubCategories[subcategory]; + } + public static bool IsSubEditor() => Screen.Selected is SubEditorScreen && !Submarine.Unloading; public static bool IsWiringMode() => Screen.Selected == GameMain.SubEditorScreen && GameMain.SubEditorScreen.WiringMode && !Submarine.Unloading; + } } 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/OggSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs index 01a06e4ec..e2f733817 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/OggSound.cs @@ -41,7 +41,7 @@ namespace Barotrauma.Sounds //MuffleBuffer(floatBuffer, reader.Channels); CastBuffer(floatBuffer, buffer, readSamples); - return readSamples * 2; + return readSamples; } static void MuffleBuffer(float[] buffer, int sampleRate, int channelCount) 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/SoundChannel.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs index 48a2cc7f5..4964a7053 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs @@ -736,11 +736,6 @@ namespace Barotrauma.Sounds if (FilledByNetwork) { - if (Sound is VoipSound voipSound) - { - voipSound.ApplyFilters(buffer, readSamples); - } - if (readSamples <= 0) { streamAmplitude *= 0.5f; @@ -752,13 +747,18 @@ namespace Barotrauma.Sounds } else { + if (Sound is VoipSound voipSound) + { + voipSound.ApplyFilters(buffer, readSamples); + } + decayTimer = 0; } } else if (Sound.StreamsReliably) { - streamSeekPos += readSamples; - if (readSamples < STREAM_BUFFER_SIZE) + streamSeekPos += readSamples * 2; + if (readSamples * 2 < STREAM_BUFFER_SIZE) { if (looping) { @@ -775,7 +775,7 @@ namespace Barotrauma.Sounds { streamBufferAmplitudes[index] = readAmplitude; - Al.BufferData(streamBuffers[index], Sound.ALFormat, buffer, readSamples, Sound.SampleRate); + Al.BufferData(streamBuffers[index], Sound.ALFormat, buffer, readSamples * 2, Sound.SampleRate); alError = Al.GetError(); if (alError != Al.NoError) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs index 03420baa7..1718a33f3 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; } @@ -706,9 +706,11 @@ namespace Barotrauma.Sounds } bool areStreamsPlaying = false; + ManualResetEvent streamMre = null; void UpdateStreaming() { + streamMre = new ManualResetEvent(false); bool killThread = false; while (!killThread) { @@ -745,14 +747,20 @@ namespace Barotrauma.Sounds } } } + streamMre.WaitOne(10); + streamMre.Reset(); lock (threadDeathMutex) { areStreamsPlaying = !killThread; } - Thread.Sleep(10); //TODO: use a separate thread for network audio? } } + public void ForceStreamUpdate() + { + streamMre?.Set(); + } + private void ReloadSounds() { for (int i = loadedSounds.Count - 1; i >= 0; i--) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs index 02b85c59f..78349f5eb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs @@ -195,14 +195,21 @@ namespace Barotrauma { case "music": var newMusicClip = new BackgroundMusic(soundElement); - musicClips.AddIfNotNull(newMusicClip); - if (loadedSoundElements != null) + if (File.Exists(newMusicClip.File)) { - if (newMusicClip.Type.Equals("menu", StringComparison.OrdinalIgnoreCase)) + musicClips.AddIfNotNull(newMusicClip); + if (loadedSoundElements != null) { - targetMusic[0] = newMusicClip; + if (newMusicClip.Type.Equals("menu", StringComparison.OrdinalIgnoreCase)) + { + targetMusic[0] = newMusicClip; + } } } + else + { + DebugConsole.NewMessage($"Music file \"{newMusicClip.File}\" not found."); + } break; case "splash": SplashSounds.AddIfNotNull(GameMain.SoundManager.LoadSound(soundElement, false)); @@ -411,7 +418,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 +466,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 +495,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 +778,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 +793,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); } @@ -875,7 +894,17 @@ namespace Barotrauma if (currentMusic[i] == null || (musicChannel[i] == null || !musicChannel[i].IsPlaying)) { DisposeMusicChannel(i); - currentMusic[i] = GameMain.SoundManager.LoadSound(targetMusic[i].File, true); + try + { + currentMusic[i] = GameMain.SoundManager.LoadSound(targetMusic[i].File, true); + } + catch (System.IO.InvalidDataException e) + { + DebugConsole.ThrowError($"Failed to load the music clip \"{targetMusic[i].File}\".", e); + musicClips.Remove(targetMusic[i]); + targetMusic[i] = null; + break; + } musicChannel[i] = currentMusic[i].Play(0.0f, "music"); if (targetMusic[i].ContinueFromPreviousTime) { @@ -959,7 +988,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(); @@ -971,13 +1000,17 @@ namespace Barotrauma } Submarine targetSubmarine = Character.Controlled?.Submarine; - if ((targetSubmarine != null && targetSubmarine.AtDamageDepth) || - (GameMain.GameScreen != null && Screen.Selected == GameMain.GameScreen && Level.Loaded != null && Level.Loaded.GetRealWorldDepth(GameMain.GameScreen.Cam.Position.Y) > Level.Loaded.RealWorldCrushDepth)) + if (targetSubmarine != null && targetSubmarine.AtDamageDepth) + { + return "deep"; + } + if (GameMain.GameScreen != null && Screen.Selected == GameMain.GameScreen && Submarine.MainSub != null && + Level.Loaded != null && Level.Loaded.GetRealWorldDepth(GameMain.GameScreen.Cam.Position.Y) > Submarine.MainSub.RealWorldCrushDepth) { return "deep"; } - if (targetSubmarine != null) + if (targetSubmarine != null) { float floodedArea = 0.0f; float totalArea = 0.0f; @@ -1001,7 +1034,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) { @@ -1025,7 +1058,8 @@ namespace Barotrauma { return "levelend"; } - if (Timing.TotalTime < GameMain.GameSession.RoundStartTime + 120.0) + if (Timing.TotalTime < GameMain.GameSession.RoundStartTime + 120.0 && + Level.Loaded?.Type == LevelData.LevelType.LocationConnection) { return "start"; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/VideoSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/VideoSound.cs index 11bf78a85..d1a282e15 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/VideoSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/VideoSound.cs @@ -107,7 +107,7 @@ namespace Barotrauma.Sounds readAmount += buf.Length; } } - return readAmount*2; + return readAmount; } public override void Dispose() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs index 072a38cfe..35d39dc91 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs @@ -1,4 +1,5 @@ -using Barotrauma.Networking; +using Barotrauma.IO; +using Barotrauma.Networking; using Microsoft.Xna.Framework; using OpenAL; using System; @@ -35,14 +36,15 @@ namespace Barotrauma.Sounds public float Near { get; private set; } public float Far { get; private set; } - private static BiQuad[] muffleFilters = new BiQuad[] + private BiQuad[] muffleFilters = new BiQuad[] { new LowpassFilter(VoipConfig.FREQUENCY, 800) }; - private static BiQuad[] radioFilters = new BiQuad[] + private BiQuad[] radioFilters = new BiQuad[] { new BandpassFilter(VoipConfig.FREQUENCY, 2000) }; + private const float PostRadioFilterBoost = 1.2f; private float gain; public float Gain @@ -104,7 +106,7 @@ namespace Barotrauma.Sounds if (gain * GameMain.Config.VoiceChatVolume > 1.0f) //TODO: take distance into account? { - fVal = Math.Clamp(fVal * gain * GameMain.Config.VoiceChatVolume, -1.0f, 1.0f); + fVal = Math.Clamp(fVal * gain * GameMain.Config.VoiceChatVolume, -1f, 1f); } if (UseMuffleFilter) @@ -118,7 +120,7 @@ namespace Barotrauma.Sounds { foreach (var filter in radioFilters) { - fVal = filter.Process(fVal); + fVal = Math.Clamp(filter.Process(fVal) * PostRadioFilterBoost, -1f, 1f); } } buffer[i] = FloatToShort(fVal); @@ -154,7 +156,7 @@ namespace Barotrauma.Sounds { VoipConfig.Decoder.Decode(compressedBuffer, 0, compressedSize, buffer, 0, VoipConfig.BUFFER_SIZE); bufferID++; - return VoipConfig.BUFFER_SIZE * 2; + return VoipConfig.BUFFER_SIZE; } if (bufferID < queue.LatestBufferID - (VoipQueue.BUFFER_COUNT - 1)) bufferID = queue.LatestBufferID - (VoipQueue.BUFFER_COUNT - 1); } 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/Sprite/Sprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs index 01524a0ec..d2d8f6028 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs @@ -214,7 +214,7 @@ namespace Barotrauma { if (Texture == null) { return; } //Init optional values - Vector2 drawOffset = startOffset.HasValue ? startOffset.Value : Vector2.Zero; + Vector2 drawOffset = startOffset ?? Vector2.Zero; Vector2 scale = textureScale ?? Vector2.One; Color drawColor = color ?? Color.White; @@ -224,17 +224,20 @@ namespace Barotrauma //wrap the drawOffset inside the sourceRect drawOffset.X = (drawOffset.X / scale.X) % sourceRect.Width; drawOffset.Y = (drawOffset.Y / scale.Y) % sourceRect.Height; + + Vector2 flippedDrawOffset = Vector2.Zero; if (flipHorizontal) { - float diff = targetSize.X % (sourceRect.Width * scale.X); - drawOffset.X += (sourceRect.Width * scale.X - diff) / scale.X; + float diff = targetSize.X % (sourceRect.Width * scale.X); + flippedDrawOffset.X = (int)((sourceRect.Width * scale.X - diff) / scale.X); } if (flipVertical) { float diff = targetSize.Y % (sourceRect.Height * scale.Y); - drawOffset.Y += (sourceRect.Height * scale.Y - diff) / scale.Y; + flippedDrawOffset.Y = (int)((sourceRect.Height * scale.Y - diff) / scale.Y); } - + drawOffset += flippedDrawOffset; + //how many times the texture needs to be drawn on the x-axis int xTiles = (int)Math.Ceiling((targetSize.X + drawOffset.X * scale.X) / (sourceRect.Width * scale.X)); //how many times the texture needs to be drawn on the y-axis @@ -262,6 +265,10 @@ namespace Barotrauma { texPerspective.X += (int)diff; } + if (!flipVertical) + { + texPerspective.Y += (int)diff; + } } //drawing an offset flipped sprite, need to draw an extra slice to the left side if (currDrawPosition.X > position.X && x == 0) @@ -278,7 +285,7 @@ namespace Barotrauma if (flipVertical) { - slicePos.Y += size.Y; + slicePos.Y += flippedDrawOffset.Y; } spriteBatch.Draw(texture, slicePos, sliceRect, drawColor, rotation, Vector2.Zero, scale, effects, depth ?? this.depth); 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/SpreadsheetExport.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpreadsheetExport.cs new file mode 100644 index 000000000..403a70475 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpreadsheetExport.cs @@ -0,0 +1,262 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Xml.Linq; +using Barotrauma.IO; +using Barotrauma.Items.Components; + +namespace Barotrauma +{ + public class SpreadsheetExport + { + private const char separator = ','; + private const string debugIdentifier = "_spreadsheet"; + + public static void Export() + { + XDocument doc = new XDocument(); + if (doc.Root == null) + { + doc.Add(new XElement("Content")); + } + + XElement root = doc.Root!; + + foreach (ItemPrefab prefab in ItemPrefab.Prefabs) + { + XElement itemElement = new XElement("Item", + new XAttribute("identifier", prefab.Identifier), + new XAttribute("name", prefab.Name), + new XAttribute("tags", FormatArray(prefab.Tags)), + new XAttribute("value", prefab.DefaultPrice?.Price ?? 0) + ); + + itemElement.Add(ParseRecipe(prefab)); + itemElement.Add(ParseDecon(prefab)); + itemElement.Add(ParseMedical(prefab)); + itemElement.Add(ParseWeapon(prefab)); + + root.Add(itemElement); + } + + System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings + { + Indent = false, + NewLineOnAttributes = false + }; + + using XmlWriter? writer = XmlWriter.Create("spreadsheetdata.xml", settings); + doc.SaveSafe(writer); + } + + private static XElement ParseRecipe(ItemPrefab prefab) + { + FabricationRecipe? recipe = prefab.FabricationRecipes.FirstOrDefault(); + + List ingredients = recipe?.RequiredItems.SelectMany(ri => ri.ItemPrefabs).Distinct().ToList() ?? new List(); + Skill? skill = recipe?.RequiredSkills.FirstOrDefault(); + + return new XElement("Recipe", + new XAttribute("amount", recipe?.Amount ?? 0), + new XAttribute("time", recipe?.RequiredTime ?? 0), + new XAttribute("skillname", skill?.Identifier ?? ""), + new XAttribute("skillamount", (int?) skill?.Level ?? 0), + new XAttribute("ingredients", FormatArray(ingredients.Select(ip => ip.Name))), + new XAttribute("values", FormatArray(ingredients.Select(ip => ip.DefaultPrice?.Price ?? 0))) + ); + } + + private static XElement ParseDecon(ItemPrefab prefab) + { + List deconOutput = prefab.DeconstructItems.Select(item => ItemPrefab.Find(null, item.ItemIdentifier)).Where(outputPrefab => outputPrefab != null).ToList(); + return new XElement("Deconstruct", + new XAttribute("time", prefab.DeconstructTime), + new XAttribute("outputs", FormatArray(deconOutput.Select(ip => ip.Name))), + new XAttribute("values", FormatArray(deconOutput.Select(ip => ip.DefaultPrice?.Price ?? 0))) + ); + } + + private static XElement ParseMedical(ItemPrefab prefab) + { + XElement? itemMeleeWeapon = prefab.ConfigElement.GetChildElement(nameof(MeleeWeapon)); + // affliction, amount, duration + List> onSuccessAfflictions = new List>(); + List> onFailureAfflictions = new List>(); + int medicalRequiredSkill = 0; + if (itemMeleeWeapon != null) + { + List statusEffects = new List(); + foreach (XElement subElement in itemMeleeWeapon.Elements()) + { + string name = subElement.Name.ToString(); + if (name.Equals(nameof(StatusEffect), StringComparison.OrdinalIgnoreCase)) + { + StatusEffect statusEffect = StatusEffect.Load(subElement, debugIdentifier); + if (statusEffect == null || !statusEffect.HasTag("medical")) { continue; } + + statusEffects.Add(statusEffect); + } + else if (IsRequiredSkill(subElement, out Skill? skill) && skill != null) + { + medicalRequiredSkill = (int) skill.Level; + } + } + + List successEffects = statusEffects.Where(se => se.type == ActionType.OnUse).ToList(); + List failureEffects = statusEffects.Where(se => se.type == ActionType.OnFailure).ToList(); + + foreach (StatusEffect statusEffect in successEffects) + { + float duration = statusEffect.Duration; + onSuccessAfflictions.AddRange(statusEffect.ReduceAffliction.Select(pair => Tuple.Create(GetAfflictionName(pair.First), -pair.Second, duration))); + onSuccessAfflictions.AddRange(statusEffect.Afflictions.Select(affliction => Tuple.Create(affliction.Prefab.Name, affliction.NonClampedStrength, duration))); + } + + foreach (StatusEffect statusEffect in failureEffects) + { + float duration = statusEffect.Duration; + onFailureAfflictions.AddRange(statusEffect.ReduceAffliction.Select(pair => Tuple.Create(GetAfflictionName(pair.First), -pair.Second, duration))); + onFailureAfflictions.AddRange(statusEffect.Afflictions.Select(affliction => Tuple.Create(affliction.Prefab.Name, affliction.NonClampedStrength, duration))); + } + } + + return new XElement("Medical", + new XAttribute("skillamount", medicalRequiredSkill), + new XAttribute("successafflictions", FormatArray(onSuccessAfflictions.Select(tpl => tpl.Item1))), + new XAttribute("successamounts", FormatArray(onSuccessAfflictions.Select(tpl => FormatFloat(tpl.Item2)))), + new XAttribute("successdurations", FormatArray(onSuccessAfflictions.Select(tpl => FormatFloat(tpl.Item3)))), + new XAttribute("failureafflictions", FormatArray(onFailureAfflictions.Select(tpl => tpl.Item1))), + new XAttribute("failureamounts", FormatArray(onFailureAfflictions.Select(tpl => FormatFloat(tpl.Item2)))), + new XAttribute("failuredurations", FormatArray(onFailureAfflictions.Select(tpl => FormatFloat(tpl.Item3)))) + ); + } + + private static XElement ParseWeapon(ItemPrefab prefab) + { + float stun = 0; + bool isAoE = false; + float? structDamage = null; + int skillRequirement = 0; + + // affliction, amount + List> damages = new List>(); + + string[] validNames = { nameof(Projectile), nameof(MeleeWeapon), nameof(RepairTool), nameof(ItemComponent), nameof(RangedWeapon) }; + foreach (XElement icElement in prefab.ConfigElement.Elements()) + { + string icName = icElement.Name.ToString(); + if (!validNames.Any(name => icName.Equals(name, StringComparison.OrdinalIgnoreCase))) { continue; } + + foreach (XElement icChildElement in icElement.Elements()) + { + string name = icChildElement.Name.ToString(); + if (IsRequiredSkill(icChildElement, out Skill? skill) && skill != null) + { + skillRequirement = (int) skill.Level; + } + else if (name.Equals(nameof(Attack), StringComparison.OrdinalIgnoreCase)) + { + ParseAttack(new Attack(icChildElement, debugIdentifier)); + } + else if (name.Equals(nameof(Explosion), StringComparison.OrdinalIgnoreCase)) + { + ParseExplosion(new[] { new Explosion(icChildElement, debugIdentifier) }); + } + else if (name.Equals(nameof(StatusEffect), StringComparison.OrdinalIgnoreCase)) + { + ParseStatusEffect(new[] { StatusEffect.Load(icChildElement, debugIdentifier) }); + } + + void ParseStatusEffect(IEnumerable statusEffects) + { + foreach (StatusEffect effect in statusEffects) + { + if (effect.HasTargetType(StatusEffect.TargetType.Character)) { continue; } + + ParseAfflictions(effect.Afflictions); + ParseExplosion(effect.Explosions); + } + } + + void ParseExplosion(IEnumerable explosions) + { + foreach (Explosion explosion in explosions) + { + isAoE = true; + ParseAttack(explosion.Attack); + ParseStatusEffect(explosion.Attack.StatusEffects); + } + } + + void ParseAttack(Attack attack) + { + structDamage ??= attack.StructureDamage; + ParseAfflictions(attack.Afflictions.Keys); + ParseStatusEffect(attack.StatusEffects); + } + + void ParseAfflictions(IEnumerable afflictions) + { + foreach (Affliction affliction in afflictions) + { + // Exclude stuns + if (affliction.Prefab == AfflictionPrefab.Stun) + { + stun += affliction.NonClampedStrength; + continue; + } + + damages.Add(Tuple.Create(affliction.Prefab.Name, affliction.NonClampedStrength)); + } + } + } + } + + return new XElement("Weapon", + new XAttribute("damagenames", FormatArray(damages.Select(tpl => tpl.Item1))), + new XAttribute("damageamounts", FormatArray(damages.Select(tpl => FormatFloat(tpl.Item2)))), + new XAttribute("isaoe", isAoE), + new XAttribute("structuredamage", structDamage ?? 0), + new XAttribute("stun", FormatFloat(stun)), + new XAttribute("skillrequirement", skillRequirement) + ); + } + + private static string GetAfflictionName(string identifier) + { + return AfflictionPrefab.Prefabs.Find(prefab => prefab.Identifier.Equals(identifier, StringComparison.OrdinalIgnoreCase))?.Name ?? CultureInfo.CurrentCulture.TextInfo.ToTitleCase(identifier.ToLower()); + } + + private static string FormatFloat(float value) + { + return value.ToString("0.00", CultureInfo.InvariantCulture); + } + + private static string FormatArray(IEnumerable array) + { + return string.Join(separator, array); + } + + private static bool IsRequiredSkill(XElement element, out Skill? skill) + { + string name = element.Name.ToString(); + bool isSkill = name.Equals("RequiredSkill", StringComparison.OrdinalIgnoreCase) || + name.Equals("RequiredSkills", StringComparison.OrdinalIgnoreCase); + + if (isSkill) + { + string identifier = element.GetAttributeString(nameof(Skill.Identifier).ToLowerInvariant(), string.Empty); + float level = element.GetAttributeFloat(nameof(Skill.Level).ToLowerInvariant(), 0f); + skill = new Skill(identifier, level); + } + else + { + skill = null; + } + + return isSkill; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/TextureLoader.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/TextureLoader.cs index 2b7b56072..a930107d0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/TextureLoader.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/TextureLoader.cs @@ -19,6 +19,8 @@ namespace Barotrauma private set; } + private static volatile bool cancelAll = false; + public static void Init(GraphicsDevice graphicsDevice, bool needsBmp = false) { _graphicsDevice = graphicsDevice; @@ -36,6 +38,11 @@ namespace Barotrauma }); } + public static void CancelAll() + { + cancelAll = true; + } + private static byte[] CompressDxt5(byte[] data, int width, int height) { using (System.IO.MemoryStream mstream = new System.IO.MemoryStream()) @@ -220,6 +227,7 @@ namespace Barotrauma Texture2D tex = null; CrossThread.RequestExecutionOnMainThread(() => { + if (cancelAll) { return; } tex = new Texture2D(_graphicsDevice, width, height, mipmap, format); tex.SetData(textureData); }); 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/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/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index 0080258ba..b05181d0e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -26,6 +26,11 @@ namespace Barotrauma Vector2 comparePosition = recipient.SpectatePos == null ? recipient.Character.WorldPosition : recipient.SpectatePos.Value; float distance = Vector2.Distance(comparePosition, WorldPosition); + if (recipient.Character?.ViewTarget != null) + { + distance = Math.Min(distance, Vector2.Distance(recipient.Character.ViewTarget.WorldPosition, WorldPosition)); + } + float priority = 1.0f - MathUtils.InverseLerp( NetConfig.HighPrioCharacterPositionUpdateDistance, NetConfig.LowPrioCharacterPositionUpdateDistance, @@ -273,21 +278,21 @@ namespace Barotrauma switch ((NetEntityEvent.Type)extraData[0]) { case NetEntityEvent.Type.InventoryState: - msg.WriteRangedInteger(0, 0, 5); + msg.WriteRangedInteger(0, 0, 6); msg.Write(GameMain.Server.EntityEventManager.Events.Last()?.ID ?? (ushort)0); Inventory.ServerWrite(msg, c); break; case NetEntityEvent.Type.Control: - msg.WriteRangedInteger(1, 0, 5); + msg.WriteRangedInteger(1, 0, 6); Client owner = (Client)extraData[1]; msg.Write(owner != null && owner.Character == this && GameMain.Server.ConnectedClients.Contains(owner) ? owner.ID : (byte)0); break; case NetEntityEvent.Type.Status: - msg.WriteRangedInteger(2, 0, 5); + msg.WriteRangedInteger(2, 0, 6); WriteStatus(msg); break; case NetEntityEvent.Type.UpdateSkills: - msg.WriteRangedInteger(3, 0, 5); + msg.WriteRangedInteger(3, 0, 6); if (Info?.Job == null) { msg.Write((byte)0); @@ -306,15 +311,36 @@ namespace Barotrauma Limb attackLimb = extraData[1] as Limb; UInt16 targetEntityID = (UInt16)extraData[2]; int targetLimbIndex = extraData.Length > 3 ? (int)extraData[3] : 0; - msg.WriteRangedInteger(4, 0, 5); + msg.WriteRangedInteger(4, 0, 6); msg.Write((byte)(Removed ? 255 : Array.IndexOf(AnimController.Limbs, attackLimb))); msg.Write(targetEntityID); msg.Write((byte)targetLimbIndex); break; case NetEntityEvent.Type.AssignCampaignInteraction: - msg.WriteRangedInteger(5, 0, 5); + msg.WriteRangedInteger(5, 0, 6); msg.Write((byte)CampaignInteractionType); break; + case NetEntityEvent.Type.ObjectiveManagerOrderState: + msg.WriteRangedInteger(6, 0, 6); + if (!(AIController is HumanAIController controller)) + { + msg.Write(false); + break; + } + var currentOrderInfo = controller.ObjectiveManager.GetCurrentOrderInfo(); + if (!currentOrderInfo.HasValue) + { + msg.Write(false); + break; + } + msg.Write(true); + var orderPrefab = currentOrderInfo.Value.Order.Prefab; + int orderIndex = Order.PrefabList.IndexOf(orderPrefab); + msg.WriteRangedInteger(orderIndex, 0, Order.PrefabList.Count); + if (!orderPrefab.HasOptions) { break; } + int optionIndex = orderPrefab.Options.IndexOf(currentOrderInfo.Value.OrderOption); + msg.WriteRangedInteger(optionIndex, 0, orderPrefab.Options.Length); + break; default: DebugConsole.ThrowError("Invalid NetworkEvent type for entity " + ToString() + " (" + (NetEntityEvent.Type)extraData[0] + ")"); break; @@ -524,29 +550,28 @@ namespace Barotrauma msg.Write((byte)CampaignInteractionType); - // Current order - if (info.CurrentOrder != null) + + // Current orders + msg.Write((byte)info.CurrentOrders.Count(o => o.Order != null)); + foreach (var orderInfo in info.CurrentOrders) { - msg.Write(true); - msg.Write((byte)Order.PrefabList.IndexOf(info.CurrentOrder.Prefab)); - msg.Write(info.CurrentOrder.TargetEntity == null ? (UInt16)0 : info.CurrentOrder.TargetEntity.ID); - var hasOrderGiver = info.CurrentOrder.OrderGiver != null; + if (orderInfo.Order == null) { continue; } + msg.Write((byte)Order.PrefabList.IndexOf(orderInfo.Order.Prefab)); + msg.Write(orderInfo.Order.TargetEntity == null ? (UInt16)0 : orderInfo.Order.TargetEntity.ID); + var hasOrderGiver = orderInfo.Order.OrderGiver != null; msg.Write(hasOrderGiver); - if (hasOrderGiver) { msg.Write(info.CurrentOrder.OrderGiver.ID); } - msg.Write((byte)(string.IsNullOrWhiteSpace(info.CurrentOrderOption) ? 0 : Array.IndexOf(info.CurrentOrder.Prefab.Options, info.CurrentOrderOption))); - var hasTargetPosition = info.CurrentOrder.TargetPosition != null; + if (hasOrderGiver) { msg.Write(orderInfo.Order.OrderGiver.ID); } + msg.Write((byte)(string.IsNullOrWhiteSpace(orderInfo.OrderOption) ? 0 : Array.IndexOf(orderInfo.Order.Prefab.Options, orderInfo.OrderOption))); + msg.Write((byte)orderInfo.ManualPriority); + var hasTargetPosition = orderInfo.Order.TargetPosition != null; msg.Write(hasTargetPosition); if (hasTargetPosition) { - msg.Write(info.CurrentOrder.TargetPosition.Position.X); - msg.Write(info.CurrentOrder.TargetPosition.Position.Y); - msg.Write(info.CurrentOrder.TargetPosition.Hull == null ? (UInt16)0 : info.CurrentOrder.TargetPosition.Hull.ID); + msg.Write(orderInfo.Order.TargetPosition.Position.X); + msg.Write(orderInfo.Order.TargetPosition.Position.Y); + msg.Write(orderInfo.Order.TargetPosition.Hull == null ? (UInt16)0 : orderInfo.Order.TargetPosition.Hull.ID); } } - else - { - msg.Write(false); - } TryWriteStatus(msg); diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index a5b30f5d8..d35f627d4 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -73,17 +73,27 @@ namespace Barotrauma Stopwatch sw = new Stopwatch(); sw.Start(); - int consoleWidth = Console.WindowWidth; - if (consoleWidth < 5) consoleWidth = 5; - int consoleHeight = Console.WindowHeight; - if (consoleHeight < 5) consoleHeight = 5; + int consoleWidth = 0; + int consoleHeight = 0; + + if(!Console.IsOutputRedirected) + { + consoleWidth = Console.WindowWidth; + if (consoleWidth < 5) consoleWidth = 5; + consoleHeight = Console.WindowHeight; + if (consoleHeight < 5) consoleHeight = 5; + } //dequeue messages lock (queuedMessages) { if (queuedMessages.Count > 0) { - Console.CursorLeft = 0; + + if (!Console.IsOutputRedirected) + { + Console.CursorLeft = 0; + } while (queuedMessages.Count > 0) { ColoredText msg = queuedMessages.Dequeue(); @@ -102,15 +112,21 @@ namespace Barotrauma if (msg.IsCommand) commandMemory.Add(msgTxt); - int paddingLen = consoleWidth - (msg.Text.Length % consoleWidth)-1; - msgTxt += new string(' ', paddingLen>0 ? paddingLen : 0); + if(!Console.IsOutputRedirected) + { + int paddingLen = consoleWidth - (msg.Text.Length % consoleWidth) - 1; + msgTxt += new string(' ', paddingLen > 0 ? paddingLen : 0); - Console.ForegroundColor = XnaToConsoleColor.Convert(msg.Color); + Console.ForegroundColor = XnaToConsoleColor.Convert(msg.Color); + } Console.WriteLine(msgTxt); if (sw.ElapsedMilliseconds >= maxTime) { break; } } - RewriteInputToCommandLine(input); + if(!Console.IsOutputRedirected) + { + RewriteInputToCommandLine(input); + } } if (Messages.Count > MaxMessages) { @@ -118,73 +134,78 @@ namespace Barotrauma } } - //read player input - bool rewriteInput = false; - while (Console.KeyAvailable) + // No good way to display input when console output is redirected, and can't read from redirected input using KeyAvailable. + if(!Console.IsOutputRedirected && !Console.IsInputRedirected) { - if (sw.ElapsedMilliseconds >= maxTime) + //read player input + bool rewriteInput = false; + while (Console.KeyAvailable) { - rewriteInput = false; - break; - } - rewriteInput = true; - ConsoleKeyInfo key = Console.ReadKey(true); - switch (key.Key) - { - case ConsoleKey.Enter: - lock (QueuedCommands) - { - QueuedCommands.Add(input); - } - input = ""; - memoryIndex = -1; + if (sw.ElapsedMilliseconds >= maxTime) + { + rewriteInput = false; break; - case ConsoleKey.Backspace: - if (input.Length > 0) input = input.Substring(0, input.Length - 1); - memoryIndex = -1; - break; - case ConsoleKey.LeftArrow: - input = AutoComplete(input, -1); - break; - case ConsoleKey.RightArrow: - input = AutoComplete(input, 1); - break; - case ConsoleKey.UpArrow: - memoryIndex--; - if (memoryIndex < 0) memoryIndex = commandMemory.Count - 1; - if (memoryIndex >= commandMemory.Count) memoryIndex = commandMemory.Count - 1; - if (memoryIndex >= 0) - { - input = commandMemory[memoryIndex]; - } - break; - case ConsoleKey.DownArrow: - memoryIndex++; - if (memoryIndex < 0) memoryIndex = 0; - if (memoryIndex >= commandMemory.Count) memoryIndex = 0; - if (commandMemory.Count>0) - { - input = commandMemory[memoryIndex]; - } - break; - case ConsoleKey.Tab: - if (input.Length > 0) - { - input = AutoComplete(input, 0); + } + rewriteInput = true; + ConsoleKeyInfo key = Console.ReadKey(true); + switch (key.Key) + { + case ConsoleKey.Enter: + lock (QueuedCommands) + { + QueuedCommands.Add(input); + } + input = ""; memoryIndex = -1; - } - break; - default: - if (key.KeyChar != 0) - { - input += key.KeyChar; + break; + case ConsoleKey.Backspace: + if (input.Length > 0) input = input.Substring(0, input.Length - 1); + ResetAutoComplete(); memoryIndex = -1; - } - ResetAutoComplete(); - break; + break; + case ConsoleKey.LeftArrow: + input = AutoComplete(input, -1); + break; + case ConsoleKey.RightArrow: + input = AutoComplete(input, 1); + break; + case ConsoleKey.UpArrow: + memoryIndex--; + if (memoryIndex < 0) memoryIndex = commandMemory.Count - 1; + if (memoryIndex >= commandMemory.Count) memoryIndex = commandMemory.Count - 1; + if (memoryIndex >= 0) + { + input = commandMemory[memoryIndex]; + } + break; + case ConsoleKey.DownArrow: + memoryIndex++; + if (memoryIndex < 0) memoryIndex = 0; + if (memoryIndex >= commandMemory.Count) memoryIndex = 0; + if (commandMemory.Count>0) + { + input = commandMemory[memoryIndex]; + } + break; + case ConsoleKey.Tab: + if (input.Length > 0) + { + input = AutoComplete(input, 0); + memoryIndex = -1; + } + break; + default: + if (key.KeyChar != 0) + { + input += key.KeyChar; + memoryIndex = -1; + } + ResetAutoComplete(); + break; + } } + if (rewriteInput) { RewriteInputToCommandLine(input); } } - if (rewriteInput) { RewriteInputToCommandLine(input); } sw.Stop(); } @@ -1333,7 +1354,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) => @@ -1708,14 +1737,15 @@ namespace Barotrauma (Client client, Vector2 cursorWorldPos, string[] args) => { Vector2 explosionPos = cursorWorldPos; - float range = 500, force = 10, damage = 50, structureDamage = 10, itemDamage = 100, empStrength = 0.0f; ; + float range = 500, force = 10, damage = 50, structureDamage = 10, itemDamage = 100, empStrength = 0.0f, ballastFloraStrength = 50f; if (args.Length > 0) float.TryParse(args[0], out range); if (args.Length > 1) float.TryParse(args[1], out force); if (args.Length > 2) float.TryParse(args[2], out damage); if (args.Length > 3) float.TryParse(args[3], out structureDamage); if (args.Length > 4) float.TryParse(args[4], out itemDamage); if (args.Length > 5) float.TryParse(args[5], out empStrength); - new Explosion(range, force, damage, structureDamage, itemDamage, empStrength).Explode(explosionPos, null); + if (args.Length > 6) float.TryParse(args[6], out ballastFloraStrength); + new Explosion(range, force, damage, structureDamage, itemDamage, empStrength, ballastFloraStrength).Explode(explosionPos, null); } ); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/AbandonedOutpostMission.cs new file mode 100644 index 000000000..1a978d069 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/AbandonedOutpostMission.cs @@ -0,0 +1,28 @@ +using Barotrauma.Networking; +using System; +using System.Linq; + +namespace Barotrauma +{ + partial class AbandonedOutpostMission : Mission + { + public override void ServerWriteInitial(IWriteMessage msg, Client c) + { + if (characters.Count == 0) + { + throw new InvalidOperationException("Server attempted to write AbandonedOutpostMission data when no characters had been spawned."); + } + + msg.Write((byte)characters.Count); + foreach (Character character in characters) + { + character.WriteSpawnData(msg, character.ID, restrictMessageSize: false); + msg.Write((ushort)characterItems[character].Count()); + foreach (Item item in characterItems[character]) + { + item.WriteSpawnData(msg, item.ID, item.ParentInventory.Owner?.ID ?? Entity.NullEntityID, 0); + } + } + } + } +} \ No newline at end of file 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/GameMain.cs b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs index 5cf61396e..6abe50855 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -361,7 +361,7 @@ namespace Barotrauma } #if !DEBUG - if (Server?.OwnerConnection == null && !Console.IsOutputRedirected) + if (Server?.OwnerConnection == null) { DebugConsole.UpdateCommandLine((int)(Timing.Accumulator * 800)); } 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/CampaignMode.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs index fc8f7e167..9c3d14d77 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs @@ -1,5 +1,4 @@ using Barotrauma.Networking; -using System.Collections.Generic; namespace Barotrauma { @@ -13,10 +12,11 @@ namespace Barotrauma public override void ShowStartMessage() { - if (Mission == null) return; - - GameServer.Log(TextManager.Get("Mission") + ": " + Mission.Name, Networking.ServerLog.MessageType.ServerMessage); - GameServer.Log(Mission.Description, Networking.ServerLog.MessageType.ServerMessage); + foreach (Mission mission in Missions) + { + GameServer.Log(TextManager.Get("Mission") + ": " + mission.Name, ServerLog.MessageType.ServerMessage); + GameServer.Log(mission.Description, ServerLog.MessageType.ServerMessage); + } } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MissionMode.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MissionMode.cs index 4d36c08d4..51ad5e730 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MissionMode.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MissionMode.cs @@ -4,10 +4,11 @@ { public override void ShowStartMessage() { - if (mission == null) return; - - Networking.GameServer.Log(TextManager.Get("Mission") + ": " + mission.Name, Networking.ServerLog.MessageType.ServerMessage); - Networking.GameServer.Log(mission.Description, Networking.ServerLog.MessageType.ServerMessage); + foreach (Mission mission in missions) + { + Networking.GameServer.Log(TextManager.Get("Mission") + ": " + mission.Name, Networking.ServerLog.MessageType.ServerMessage); + Networking.GameServer.Log(mission.Description, Networking.ServerLog.MessageType.ServerMessage); + } } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index 0171ac8f1..bdc4e9f7d 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) @@ -367,6 +367,8 @@ namespace Barotrauma { if (CoroutineManager.IsCoroutineRunning("LevelTransition")) { return; } + Map?.Radiation.UpdateRadiation(deltaTime); + base.Update(deltaTime); if (Level.Loaded != null) { @@ -441,9 +443,16 @@ namespace Barotrauma foreach (Mission mission in map.CurrentLocation.AvailableMissions) { msg.Write(mission.Prefab.Identifier); - Location missionDestination = mission.Locations[0] == map.CurrentLocation ? mission.Locations[1] : mission.Locations[0]; - LocationConnection connection = map.CurrentLocation.Connections.Find(c => c.OtherLocation(map.CurrentLocation) == missionDestination); - msg.Write((byte)map.CurrentLocation.Connections.IndexOf(connection)); + if (mission.Locations[0] == mission.Locations[1]) + { + msg.Write((byte)255); + } + else + { + Location missionDestination = mission.Locations[0] == map.CurrentLocation ? mission.Locations[1] : mission.Locations[0]; + LocationConnection connection = map.CurrentLocation.Connections.Find(c => c.OtherLocation(map.CurrentLocation) == missionDestination); + msg.Write((byte)map.CurrentLocation.Connections.IndexOf(connection)); + } } // Store balance @@ -773,6 +782,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/GameSession/ReadyCheck.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/ReadyCheck.cs index 21ce1491d..dea506acf 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/ReadyCheck.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/ReadyCheck.cs @@ -64,6 +64,7 @@ namespace Barotrauma partial void EndReadyCheck() { + if (IsFinished) { return; } IsFinished = true; foreach (Client client in ActivePlayers) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/DockingPort.cs index 72d130ff1..eb8aa8e58 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/DockingPort.cs @@ -5,17 +5,14 @@ namespace Barotrauma.Items.Components { partial class DockingPort : ItemComponent, IDrawableComponent, IServerSerializable { - - private UInt16 originalDockingTargetID; - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { msg.Write(docked); if (docked) { - msg.Write(originalDockingTargetID); - msg.Write(hulls != null && hulls[0] != null && hulls[1] != null && gap != null); + msg.Write(DockingTarget.item.ID); + 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/Machines/Pump.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Pump.cs index 176ca9a8d..74ba0b7ce 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Pump.cs @@ -42,6 +42,15 @@ namespace Barotrauma.Items.Components msg.WriteRangedInteger((int)(flowPercentage / 10.0f), -10, 10); msg.Write(IsActive); msg.Write(Hijacked); + if (TargetLevel != null) + { + msg.Write(true); + msg.Write(TargetLevel.Value); + } + else + { + msg.Write(false); + } } } } 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/Items/Item.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs index fc0b06a65..be554ba94 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs @@ -240,8 +240,6 @@ namespace Barotrauma { if (GameMain.Server == null) { return; } - int initialLength = msg.LengthBytes; - msg.Write(Prefab.OriginalName); msg.Write(Prefab.Identifier); msg.Write(Description != prefab.Description); @@ -294,11 +292,6 @@ namespace Barotrauma { msg.Write(nameTag.WrittenName ?? ""); } - - if (msg.LengthBytes - initialLength >= 255) - { - DebugConsole.ThrowError($"Too much data in an item spawn message. Item: \"{Prefab.Identifier}\", msg bytes: {(msg.LengthBytes - initialLength)}, description changed: {(Description != prefab.Description)}, description: {Description}, tags changed: {tagsChanged}, tags: {Tags}"); - } } partial void UpdateNetPosition(float deltaTime) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs index c8e780d8b..2e513b7fa 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs @@ -58,10 +58,14 @@ namespace Barotrauma.MapCreatures.Behavior msg.Write(branch.Health); } - public void ServerWriteInfect(IWriteMessage msg, UInt16 itemID, bool infect) + public void ServerWriteInfect(IWriteMessage msg, UInt16 itemID, bool infect, BallastFloraBranch infector = null) { msg.Write(itemID); msg.Write(infect); + if (infect) + { + msg.Write(infector?.ID ?? -1); + } } public void ServerWriteBranchRemove(IWriteMessage msg, BallastFloraBranch branch) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs index 74876a5ec..e7744bf45 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs @@ -93,7 +93,9 @@ namespace Barotrauma behavior.ServerWriteBranchRemove(message, branch); break; case BallastFloraBehavior.NetworkHeader.Infect when extraData.Length >= 4 && extraData[2] is UInt16 itemID && extraData[3] is bool infect: - behavior.ServerWriteInfect(message, itemID, infect); + BallastFloraBranch infector = null; + if (extraData.Length >= 5 && extraData[4] is BallastFloraBranch b) { infector = b; } + behavior.ServerWriteInfect(message, itemID, infect, infector); break; } @@ -223,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; } @@ -240,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/BanList.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs index 83b790157..a9ece25b7 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs @@ -320,7 +320,17 @@ namespace Barotrauma.Networking outMsg.Write(bannedPlayer.Name); outMsg.Write(bannedPlayer.UniqueIdentifier); - outMsg.Write(bannedPlayer.IsRangeBan); outMsg.WritePadBits(); + outMsg.Write(bannedPlayer.IsRangeBan); + outMsg.Write(bannedPlayer.ExpirationTime != null); + outMsg.WritePadBits(); + if (bannedPlayer.ExpirationTime != null) + { + double hoursFromNow = (bannedPlayer.ExpirationTime.Value - DateTime.Now).TotalHours; + outMsg.Write(hoursFromNow); + } + + outMsg.Write(bannedPlayer.Reason ?? ""); + if (c.Connection == GameMain.Server.OwnerConnection) { outMsg.Write(bannedPlayer.EndPoint); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs index d6ea2bad9..f23528e1d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs @@ -25,7 +25,54 @@ namespace Barotrauma.Networking int orderIndex = msg.ReadByte(); orderTargetCharacter = Entity.FindEntityByID(msg.ReadUInt16()) as Character; orderTargetEntity = Entity.FindEntityByID(msg.ReadUInt16()) as Entity; - int orderOptionIndex = msg.ReadByte(); + + Order orderPrefab = null; + int? orderOptionIndex = null; + string orderOption = null; + + // The option of a Dismiss order is written differently so we know what order we target + // now that the game supports multiple current orders simultaneously + if (orderIndex >= 0 && orderIndex < Order.PrefabList.Count) + { + orderPrefab = Order.PrefabList[orderIndex]; + if (orderPrefab.Identifier != "dismissed") + { + orderOptionIndex = msg.ReadByte(); + } + // Does the dismiss order have a specified target? + else if(msg.ReadBoolean()) + { + int identifierCount = msg.ReadByte(); + if (identifierCount > 0) + { + int dismissedOrderIndex = msg.ReadByte(); + Order dismissedOrderPrefab = null; + if (dismissedOrderIndex >= 0 && dismissedOrderIndex < Order.PrefabList.Count) + { + dismissedOrderPrefab = Order.PrefabList[dismissedOrderIndex]; + orderOption = dismissedOrderPrefab.Identifier; + } + if (identifierCount > 1) + { + int dismissedOrderOptionIndex = msg.ReadByte(); + if (dismissedOrderPrefab != null) + { + var options = dismissedOrderPrefab.Options; + if (options != null && dismissedOrderOptionIndex >= 0 && dismissedOrderOptionIndex < options.Length) + { + orderOption += $".{options[dismissedOrderOptionIndex]}"; + } + } + } + } + } + } + else + { + orderOptionIndex = msg.ReadByte(); + } + + int orderPriority = msg.ReadByte(); orderTargetType = (Order.OrderTargetType)msg.ReadByte(); if (msg.ReadBoolean()) { @@ -41,14 +88,14 @@ namespace Barotrauma.Networking if (orderIndex < 0 || orderIndex >= Order.PrefabList.Count) { - DebugConsole.ThrowError($"Invalid order message from client \"{c.Name}\" - order index out of bounds ({orderIndex}, {orderOptionIndex})."); + DebugConsole.ThrowError($"Invalid order message from client \"{c.Name}\" - order index out of bounds ({orderIndex})."); if (NetIdUtils.IdMoreRecent(ID, c.LastSentChatMsgID)) { c.LastSentChatMsgID = ID; } return; } - Order orderPrefab = Order.PrefabList[orderIndex]; - string orderOption = orderOptionIndex < 0 || orderOptionIndex >= orderPrefab.Options.Length ? "" : orderPrefab.Options[orderOptionIndex]; - orderMsg = new OrderChatMessage(orderPrefab, orderOption, orderTargetPosition ?? orderTargetEntity as ISpatialEntity, orderTargetCharacter, c.Character) + orderPrefab ??= Order.PrefabList[orderIndex]; + orderOption ??= orderOptionIndex == null || orderOptionIndex < 0 || orderOptionIndex >= orderPrefab.Options.Length ? "" : orderPrefab.Options[orderOptionIndex.Value]; + orderMsg = new OrderChatMessage(orderPrefab, orderOption, orderPriority, orderTargetPosition ?? orderTargetEntity as ISpatialEntity, orderTargetCharacter, c.Character) { WallSectionIndex = wallSectionIndex }; @@ -147,7 +194,7 @@ namespace Barotrauma.Networking } if (order != null) { - orderTargetCharacter.SetOrder(order, orderMsg.OrderOption, orderMsg.Sender); + orderTargetCharacter.SetOrder(order, orderMsg.OrderOption, orderMsg.OrderPriority, orderMsg.Sender); } } else if (orderMsg.Order.IsIgnoreOrder) @@ -155,11 +202,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 be1bb278a..27b949beb 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 @@ -912,7 +912,10 @@ namespace Barotrauma.Networking errorLines.Add("Campaign ID: " + campaign.CampaignID); errorLines.Add("Campaign save ID: " + campaign.LastSaveID); } - errorLines.Add("Mission: " + (GameMain.GameSession?.Mission?.Prefab.Identifier ?? "none")); + foreach (Mission mission in GameMain.GameSession.Missions) + { + errorLines.Add("Mission: " + mission.Prefab.Identifier); + } } if (GameMain.GameSession?.Submarine != null) { @@ -1247,22 +1250,27 @@ 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: @@ -1546,14 +1554,16 @@ namespace Barotrauma.Networking if (!character.Enabled) { continue; } if (c.SpectatePos == null) { - if (c.Character != null && Vector2.DistanceSquared(character.WorldPosition, c.Character.WorldPosition) >= NetConfig.DisableCharacterDistSqr) + float distSqr = Vector2.DistanceSquared(character.WorldPosition, c.Character.WorldPosition); + if (c.Character.ViewTarget != null) { - continue; + distSqr = Math.Min(distSqr, Vector2.DistanceSquared(character.WorldPosition, c.Character.ViewTarget.WorldPosition)); } + if (distSqr >= MathUtils.Pow2(character.Params.DisableDistance)) { continue; } } else { - if (character != c.Character && Vector2.DistanceSquared(character.WorldPosition, c.SpectatePos.Value) >= NetConfig.DisableCharacterDistSqr) + if (character != c.Character && Vector2.DistanceSquared(character.WorldPosition, c.SpectatePos.Value) >= MathUtils.Pow2(character.Params.DisableDistance)) { continue; } @@ -1644,6 +1654,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); @@ -1740,6 +1751,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)) { @@ -2066,14 +2078,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) @@ -2101,7 +2113,6 @@ namespace Barotrauma.Networking Log("Game mode: " + selectedMode.Name, ServerLog.MessageType.ServerMessage); Log("Submarine: " + GameMain.GameSession.SubmarineInfo.Name, ServerLog.MessageType.ServerMessage); Log("Level seed: " + campaign.NextLevel.Seed, ServerLog.MessageType.ServerMessage); - if (GameMain.GameSession.Mission != null) { Log("Mission: " + GameMain.GameSession.Mission.Prefab.Name, ServerLog.MessageType.ServerMessage); } } else { @@ -2111,7 +2122,11 @@ namespace Barotrauma.Networking Log("Game mode: " + selectedMode.Name, ServerLog.MessageType.ServerMessage); Log("Submarine: " + selectedSub.Name, ServerLog.MessageType.ServerMessage); Log("Level seed: " + GameMain.NetLobbyScreen.LevelSeed, ServerLog.MessageType.ServerMessage); - if (GameMain.GameSession.Mission != null) { Log("Mission: " + GameMain.GameSession.Mission.Prefab.Name, ServerLog.MessageType.ServerMessage); } + } + + foreach (Mission mission in GameMain.GameSession.Missions) + { + Log("Mission: " + mission.Prefab.Name, ServerLog.MessageType.ServerMessage); } if (GameMain.GameSession.SubmarineInfo.IsFileCorrupted) @@ -2123,7 +2138,7 @@ namespace Barotrauma.Networking } MissionMode missionMode = GameMain.GameSession.GameMode as MissionMode; - bool missionAllowRespawn = GameMain.GameSession.Campaign == null && (missionMode?.Mission == null || missionMode.Mission.AllowRespawn); + bool missionAllowRespawn = GameMain.GameSession.Campaign == null && (missionMode == null || !missionMode.Missions.Any(m => !m.AllowRespawn)); bool outpostAllowRespawn = GameMain.GameSession.Campaign != null && Level.Loaded?.Type == LevelData.LevelType.Outpost; if (serverSettings.AllowRespawn && (missionAllowRespawn || outpostAllowRespawn)) @@ -2145,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) @@ -2172,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); @@ -2191,6 +2206,10 @@ namespace Barotrauma.Networking { client.CharacterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, client.Name); } + else + { + client.CharacterInfo.ClearCurrentOrders(); + } characterInfos.Add(client.CharacterInfo); if (client.CharacterInfo.Job == null || client.CharacterInfo.Job.Prefab != client.AssignedJob.First) { @@ -2232,7 +2251,9 @@ namespace Barotrauma.Networking List spawnWaypoints = null; List mainSubWaypoints = WayPoint.SelectCrewSpawnPoints(characterInfos, Submarine.MainSubs[n]).ToList(); - if (Level.Loaded?.StartOutpost != null && Level.Loaded.Type == LevelData.LevelType.Outpost && + if (Level.Loaded?.StartOutpost != null && + Level.Loaded.Type == LevelData.LevelType.Outpost && + (Level.Loaded.StartOutpost.Info.OutpostGenerationParams?.SpawnCrewInsideOutpost ?? false) && Level.Loaded.StartOutpost.GetConnectedSubs().Any(s => s.Info.Type == SubmarineType.Player)) { spawnWaypoints = WayPoint.WayPointList.FindAll(wp => @@ -2291,16 +2312,25 @@ namespace Barotrauma.Networking } } - if (crewManager != null && crewManager.HasBots && hadBots) + if (crewManager != null && crewManager.HasBots) { - crewManager?.InitRound(); + if (hadBots) + { + //loaded existing bots -> init them + crewManager?.InitRound(); + } + else + { + //created new bots -> save them + SaveUtil.SaveGame(GameMain.GameSession.SavePath); + } } campaign?.LoadPets(); foreach (Submarine sub in Submarine.MainSubs) { - if (sub == null) continue; + if (sub == null) { continue; } List spawnList = new List(); foreach (KeyValuePair kvp in serverSettings.ExtraCargo) @@ -2308,7 +2338,7 @@ namespace Barotrauma.Networking spawnList.Add(new PurchasedItem(kvp.Key, kvp.Value)); } - CargoManager.CreateItems(spawnList); + CargoManager.CreateItems(spawnList, sub); } TraitorManager = null; @@ -2363,8 +2393,7 @@ namespace Barotrauma.Networking msg.Write((byte)ServerPacketHeader.STARTGAME); msg.Write(seed); msg.Write(gameSession.GameMode.Preset.Identifier); - - bool missionAllowRespawn = campaign == null && (missionMode?.Mission == null || missionMode.Mission.AllowRespawn); + bool missionAllowRespawn = campaign == null && (missionMode == null || !missionMode.Missions.Any(m => !m.AllowRespawn)); bool outpostAllowRespawn = campaign != null && campaign.NextLevel?.Type == LevelData.LevelType.Outpost; msg.Write(serverSettings.AllowRespawn && (missionAllowRespawn || outpostAllowRespawn)); msg.Write(serverSettings.AllowDisguises); @@ -2384,7 +2413,11 @@ namespace Barotrauma.Networking msg.Write(gameSession.SubmarineInfo.MD5Hash.Hash); msg.Write(GameMain.NetLobbyScreen.SelectedShuttle.Name); msg.Write(GameMain.NetLobbyScreen.SelectedShuttle.MD5Hash.Hash); - msg.Write((short)(GameMain.GameSession.GameMode?.Mission == null ? -1 : MissionPrefab.List.IndexOf(GameMain.GameSession.GameMode.Mission.Prefab))); + msg.Write((byte)GameMain.GameSession.GameMode.Missions.Count()); + foreach (Mission mission in GameMain.GameSession.GameMode.Missions) + { + msg.Write((short)MissionPrefab.List.IndexOf(mission.Prefab)); + } } else { @@ -2424,13 +2457,20 @@ namespace Barotrauma.Networking msg.Write(contentFile.Path); } msg.Write(Submarine.MainSub?.Info.EqualityCheckVal ?? 0); - msg.Write(GameMain.GameSession.Mission?.Prefab.Identifier ?? ""); + msg.Write((byte)GameMain.GameSession.Missions.Count()); + foreach (Mission mission in GameMain.GameSession.Missions) + { + msg.Write(mission.Prefab.Identifier); + } msg.Write((byte)GameMain.GameSession.Level.EqualityCheckValues.Count); foreach (int equalityCheckValue in GameMain.GameSession.Level.EqualityCheckValues) { msg.Write(equalityCheckValue); } - GameMain.GameSession.Mission?.ServerWriteInitial(msg, client); + foreach (Mission mission in GameMain.GameSession.Missions) + { + mission.ServerWriteInitial(msg, client); + } } public void EndGame(CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None) @@ -2453,7 +2493,7 @@ namespace Barotrauma.Networking string endMessage = TextManager.FormatServerMessage("RoundSummaryRoundHasEnded"); var traitorResults = TraitorManager?.GetEndResults() ?? new List(); - Mission mission = GameMain.GameSession.Mission; + List missions = GameMain.GameSession.Missions.ToList(); if (GameMain.GameSession.IsRunning) { GameMain.GameSession.EndRound(endMessage, traitorResults); @@ -2495,7 +2535,11 @@ namespace Barotrauma.Networking msg.Write((byte)ServerPacketHeader.ENDGAME); msg.Write((byte)transitionType); msg.Write(endMessage); - msg.Write(mission != null && mission.Completed); + msg.Write((byte)missions.Count); + foreach (Mission mission in missions) + { + msg.Write(mission.Completed); + } msg.Write(GameMain.GameSession?.WinningTeam == null ? (byte)0 : (byte)GameMain.GameSession.WinningTeam); msg.Write((byte)traitorResults.Count); @@ -2507,6 +2551,7 @@ namespace Barotrauma.Networking foreach (Client client in connectedClients) { serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); + client.Character?.Info?.ClearCurrentOrders(); client.Character = null; client.HasSpawned = false; client.InGame = false; @@ -2543,13 +2588,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 @@ -2663,7 +2710,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) @@ -2672,6 +2718,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)) @@ -2712,8 +2784,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}"; @@ -2923,14 +2995,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 @@ -2949,7 +3021,7 @@ namespace Barotrauma.Networking else if (type == ChatMessageType.Radio) { //send to chat-linked wifi components - senderRadio.TransmitSignal(0, message, senderRadio.Item, senderCharacter, false); + senderRadio.TransmitSignal(0, message, senderRadio.Item, senderCharacter, sentFromChat: true); } //check which clients can receive the message and apply distance effects @@ -3022,14 +3094,14 @@ namespace Barotrauma.Networking if (!client.Character.CanHearCharacter(message.Sender)) { continue; } } - SendDirectChatMessage(new OrderChatMessage(message.Order, message.OrderOption, message.TargetEntity, message.TargetCharacter, message.Sender), client); + SendDirectChatMessage(new OrderChatMessage(message.Order, message.OrderOption, message.OrderPriority, message.TargetEntity, message.TargetCharacter, message.Sender), client); } string myReceivedMessage = message.Text; if (!string.IsNullOrWhiteSpace(myReceivedMessage)) { - AddChatMessage(new OrderChatMessage(message.Order, message.OrderOption, myReceivedMessage, message.TargetEntity, message.TargetCharacter, message.Sender)); + AddChatMessage(new OrderChatMessage(message.Order, message.OrderOption, message.OrderPriority, myReceivedMessage, message.TargetEntity, message.TargetCharacter, message.Sender)); } } @@ -3145,8 +3217,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); @@ -3374,7 +3446,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 @@ -3411,9 +3483,9 @@ namespace Barotrauma.Networking unassigned.RemoveAt(i); } - //go throught the jobs whose MinNumber>0 (i.e. at least one crew member has to have the job) + // Assign the necessary jobs that are always required at least one, in vanilla this means in practice the captain bool unassignedJobsFound = true; - while (unassignedJobsFound && unassigned.Count > 0) + while (unassignedJobsFound && unassigned.Any()) { unassignedJobsFound = false; @@ -3421,16 +3493,33 @@ namespace Barotrauma.Networking { if (unassigned.Count == 0) { break; } if (jobPrefab.MinNumber < 1 || assignedClientCount[jobPrefab] >= jobPrefab.MinNumber) { continue; } + // Find the client that wants the job the most, don't force any jobs yet, because it might be that we can meet the preference for other jobs. + Client client = FindClientWithJobPreference(unassigned, jobPrefab, forceAssign: false); + if (client != null) + { + AssignJob(client, jobPrefab); + } + } - //find the client that wants the job the most, or force it to random client if none of them want it - Client assignedClient = FindClientWithJobPreference(unassigned, jobPrefab, true); + if (unassigned.Any()) + { + // Another pass, force required jobs that are not yet filled. + foreach (JobPrefab jobPrefab in jobList) + { + if (unassigned.Count == 0) { break; } + if (jobPrefab.MinNumber < 1 || assignedClientCount[jobPrefab] >= jobPrefab.MinNumber) { continue; } + AssignJob(FindClientWithJobPreference(unassigned, jobPrefab, forceAssign: true), jobPrefab); + } + } - assignedClient.AssignedJob = - assignedClient.JobPreferences.FirstOrDefault(jp => jp.First == jobPrefab) ?? - new Pair(jobPrefab, 0); + void AssignJob(Client client, JobPrefab jobPrefab) + { + client.AssignedJob = + client.JobPreferences.FirstOrDefault(jp => jp.First == jobPrefab) ?? + new Pair(jobPrefab, Rand.Int(jobPrefab.Variants)); assignedClientCount[jobPrefab]++; - unassigned.Remove(assignedClient); + unassigned.Remove(client); //the job still needs more crew members, set unassignedJobsFound to true to keep the while loop running if (assignedClientCount[jobPrefab] < jobPrefab.MinNumber) { unassignedJobsFound = true; } @@ -3464,32 +3553,37 @@ namespace Barotrauma.Networking } } while (unassigned.Count > 0 && canAssign);*/ - //attempt to give the clients a job they have in their job preferences - for (int i = unassigned.Count - 1; i >= 0; i--) + // Attempt to give the clients a job they have in their job preferences. + // First evaluate all the primary preferences, then all the secondary etc. + for (int preferenceIndex = 0; preferenceIndex < 3; preferenceIndex++) { - if (unassignedSpawnPoints.Count == 0) { break; } - foreach (Pair preferredJob in unassigned[i].JobPreferences) + if (unassignedSpawnPoints.None()) { break; } + for (int i = unassigned.Count - 1; i >= 0; i--) { - //can't assign this job if maximum number has reached or the clien't karma is too low - if (assignedClientCount[preferredJob.First] >= preferredJob.First.MaxNumber || unassigned[i].Karma < preferredJob.First.MinKarma) + if (unassignedSpawnPoints.None()) { break; } + Client client = unassigned[i]; + if (preferenceIndex >= client.JobPreferences.Count) { continue; } + var preferredJob = client.JobPreferences[preferenceIndex]; + JobPrefab jobPrefab = preferredJob.First; + if (assignedClientCount[jobPrefab] >= jobPrefab.MaxNumber || client.Karma < jobPrefab.MinKarma) { + //can't assign this job if maximum number has reached or the clien't karma is too low continue; } //give the client their preferred job if there's a spawnpoint available for that job - var matchingSpawnPoint = unassignedSpawnPoints.Find(s => s.AssignedJob == preferredJob.First); - //if the job is not available in any spawnpoint (custom job?), treat empty spawnpoints - //as a matching ones - if (matchingSpawnPoint == null && !availableSpawnPoints.Any(s => s.AssignedJob == preferredJob.First)) + var matchingSpawnPoint = unassignedSpawnPoints.Find(s => s.AssignedJob == jobPrefab); + if (matchingSpawnPoint == null && !availableSpawnPoints.Any(s => s.AssignedJob == jobPrefab)) { + //if the job is not available in any spawnpoint (custom job?), treat empty spawnpoints + //as a matching ones matchingSpawnPoint = unassignedSpawnPoints.Find(s => s.AssignedJob == null); } if (matchingSpawnPoint != null) { unassignedSpawnPoints.Remove(matchingSpawnPoint); - unassigned[i].AssignedJob = preferredJob; - assignedClientCount[preferredJob.First]++; + client.AssignedJob = preferredJob; + assignedClientCount[jobPrefab]++; unassigned.RemoveAt(i); - break; } } } @@ -3536,7 +3630,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) @@ -3614,11 +3708,9 @@ namespace Barotrauma.Networking Client preferredClient = null; foreach (Client c in clients) { - if (c.Karma < job.MinKarma) continue; + if (c.Karma < job.MinKarma) { continue; } int index = c.JobPreferences.IndexOf(c.JobPreferences.Find(j => j.First == job)); - if (index == -1) index = 1000; - - if (preferredClient == null || index < bestPreference) + if (index > -1 && index < bestPreference) { bestPreference = index; preferredClient = c; @@ -3634,12 +3726,14 @@ namespace Barotrauma.Networking return preferredClient; } - public void UpdateMissionState(int state) + public void UpdateMissionState(Mission mission, int state) { foreach (var client in connectedClients) { IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ServerPacketHeader.MISSION); + int missionIndex = GameMain.GameSession.GetMissionIndex(mission); + msg.Write((byte)(missionIndex == -1 ? 255: missionIndex)); msg.Write((ushort)state); serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); } @@ -3653,7 +3747,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/NetEntityEvent/ServerEntityEventManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs index 3ecf8c7b2..dd851251d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs @@ -490,7 +490,7 @@ namespace Barotrauma.Networking continue; } - byte msgLength = msg.ReadByte(); + int msgLength = (int)msg.ReadVariableUInt32(); IClientSerializable entity = Entity.FindEntityByID(entityID) as IClientSerializable; @@ -499,7 +499,7 @@ namespace Barotrauma.Networking { if (GameSettings.VerboseLogging) { - DebugConsole.NewMessage("Received msg " + thisEventID, Color.Red); + DebugConsole.NewMessage("Received msg " + thisEventID + ", expecting " + sender.LastSentEntityEventID, Color.Red); } msg.BitPosition += msgLength * 8; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs index dd80754d3..bdffaf263 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs @@ -1,6 +1,4 @@ -using System; - -namespace Barotrauma.Networking +namespace Barotrauma.Networking { partial class OrderChatMessage : ChatMessage { @@ -9,34 +7,13 @@ namespace Barotrauma.Networking msg.Write((byte)ServerNetObject.CHAT_MESSAGE); msg.Write(NetStateID); msg.Write((byte)ChatMessageType.Order); - msg.Write(SenderName); msg.Write(Sender != null && c.InGame); if (Sender != null && c.InGame) { msg.Write(Sender.ID); } - - msg.Write((byte)Order.PrefabList.IndexOf(Order.Prefab)); - msg.Write(TargetCharacter == null ? (UInt16)0 : TargetCharacter.ID); - msg.Write(TargetEntity is Entity ? (TargetEntity as Entity).ID : (UInt16)0); - msg.Write((byte)Array.IndexOf(Order.Prefab.Options, OrderOption)); - msg.Write((byte)Order.TargetType); - if (Order.TargetType == Order.OrderTargetType.Position && TargetEntity is OrderTarget orderTarget) - { - msg.Write(true); - msg.Write(orderTarget.Position.X); - msg.Write(orderTarget.Position.Y); - msg.Write(orderTarget.Hull == null ? (UInt16)0 : orderTarget.Hull.ID); - } - else - { - msg.Write(false); - if (Order.TargetType == Order.OrderTargetType.WallSection) - { - msg.Write((byte)(WallSectionIndex ?? Order.WallSectionIndex ?? 0)); - } - } + WriteOrder(msg); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs index d8674f03d..bfabe7aa8 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs @@ -39,7 +39,16 @@ namespace Barotrauma.Networking public double UpdateTime; public double TimeOut; public int Retries; - public UInt64? SteamID; + private UInt64? steamId; + public UInt64? SteamID + { + get { return steamId; } + set + { + steamId = value; + Connection.SetSteamIDIfUnknown(value ?? 0); + } + } public Int32? PasswordSalt; public bool AuthSessionStarted; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index be0d193bd..d2d533985 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) @@ -272,11 +276,10 @@ namespace Barotrauma.Networking { bool bot = i >= clients.Count; - characterInfos[i].CurrentOrder = null; - characterInfos[i].CurrentOrderOption = null; + characterInfos[i].ClearCurrentOrders(); 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 +351,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/Program.cs b/Barotrauma/BarotraumaServer/ServerSource/Program.cs index 2705bd8fa..ba8bb55fe 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Program.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Program.cs @@ -42,6 +42,14 @@ namespace Barotrauma #endif Console.WriteLine("Barotrauma Dedicated Server " + GameMain.Version + " (" + AssemblyInfo.BuildString + ", branch " + AssemblyInfo.GitBranch + ", revision " + AssemblyInfo.GitRevision + ")"); + if(Console.IsOutputRedirected) + { + Console.WriteLine("Output redirection detected; colored text and command input will be disabled."); + } + if(Console.IsInputRedirected) + { + Console.WriteLine("Redirected input is detected but is not supported by this application. Input will be ignored."); + } string executableDir = Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location); Directory.SetCurrentDirectory(executableDir); @@ -123,9 +131,12 @@ namespace Barotrauma sb.AppendLine("\n"); sb.AppendLine("Exception: " + exception.Message + " (" + exception.GetType().ToString() + ")"); sb.AppendLine("Target site: " +exception.TargetSite.ToString()); - sb.AppendLine("Stack trace: "); - sb.AppendLine(exception.StackTrace.CleanupStackTrace()); - sb.AppendLine("\n"); + if (exception.StackTrace != null) + { + sb.AppendLine("Stack trace: "); + sb.AppendLine(exception.StackTrace.CleanupStackTrace()); + sb.AppendLine("\n"); + } if (exception.InnerException != null) { @@ -134,8 +145,11 @@ namespace Barotrauma { sb.AppendLine("Target site: " + exception.InnerException.TargetSite.ToString()); } - sb.AppendLine("Stack trace: "); - sb.AppendLine(exception.InnerException.StackTrace.CleanupStackTrace()); + if (exception.InnerException.StackTrace != null) + { + sb.AppendLine("Stack trace: "); + sb.AppendLine(exception.InnerException.StackTrace.CleanupStackTrace()); + } } sb.AppendLine("Last debug messages:"); @@ -146,7 +160,11 @@ namespace Barotrauma } string crashReport = sb.ToString(); - Console.ForegroundColor = ConsoleColor.Red; + + if (!Console.IsOutputRedirected) + { + Console.ForegroundColor = ConsoleColor.Red; + } Console.Write(crashReport); File.WriteAllText(filePath,sb.ToString()); 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..d35216f45 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) + if (GameMain.GameSession.Missions.Any(m => m 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..1e16d660f 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) @@ -249,7 +249,7 @@ namespace Barotrauma { return; } - if (Traitors.Values.Any(traitor => traitor.Character?.IsDead ?? true || traitor.Character.Removed)) + if (Traitors.Values.Any(traitor => traitor.Character == null || traitor.Character.IsDead || traitor.Character.Removed)) { Traitors.Values.ForEach(traitor => traitor.UpdateCurrentObjective("", Identifier)); pendingObjectives.Clear(); diff --git a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml index 0806a7007..d351cd4cd 100644 --- a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml +++ b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml @@ -64,15 +64,12 @@ - - - @@ -84,11 +81,11 @@ + - @@ -100,6 +97,13 @@ + + + + + + + @@ -153,27 +157,26 @@ - + - - + - + + + + + + - - - - - @@ -207,6 +210,7 @@ + @@ -218,6 +222,8 @@ + + @@ -231,5 +237,15 @@ + + + + + + + + + + \ No newline at end of file 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..bb8e6a12f 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,177 @@ 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 UnequipEmptyItems(Item item, bool avoidDroppingInSea = true) => UnequipEmptyItems(Character, item, avoidDroppingInSea); + + public static void UnequipEmptyItems(Character character, Item item, bool avoidDroppingInSea = true) + { + if (item.OwnInventory.AllItems.Any(it => it.Condition <= 0.0f)) + { + foreach (Item containedItem in item.OwnInventory.AllItemsMod) + { + if (containedItem == null) { continue; } + if (containedItem.Condition <= 0.0f) + { + if (character.Submarine == null && avoidDroppingInSea) + { + // If we are outside of main sub, try to put the item in the inventory instead dropping it in the sea. + if (character.Inventory.TryPutItem(containedItem, character, CharacterInventory.anySlot)) + { + continue; + } + } + containedItem.Drop(character); + } + } + } + } + + 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 281722ee5..335e8fe9c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -10,30 +10,36 @@ using System.Linq; namespace Barotrauma { + public enum AIState { Idle, Attack, Escape, Eat, Flee, Avoid, Aggressive, PassiveAggressive, Protect, Observe, Freeze, Follow } + + public enum AttackPattern { Straight, Sweep, Circle } + + public enum CirclePhase { Start, CloseIn, FallBack, Advance, Strike } + 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; @@ -47,6 +53,7 @@ namespace Barotrauma private float updateMemoriesTimer; private float attackLimbResetTimer; + private bool IsAttackRunning => AttackingLimb != null && AttackingLimb.attack.IsRunning; private bool IsCoolDownRunning => AttackingLimb != null && AttackingLimb.attack.CoolDownTimer > 0; public float CombatStrength => AIParams.CombatStrength; private float Sight => AIParams.Sight; @@ -56,9 +63,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 @@ -72,6 +76,23 @@ namespace Barotrauma Reverse = _attackingLimb != null && _attackingLimb.attack.Reverse; } } + + private double lastAttackUpdateTime; + + private Attack _activeAttack; + public Attack ActiveAttack + { + get + { + if (_activeAttack == null) { return null; } + return lastAttackUpdateTime > Timing.TotalTime - _activeAttack.Duration ? _activeAttack : null; + } + private set + { + _activeAttack = value; + lastAttackUpdateTime = Timing.TotalTime; + } + } private AITargetMemory selectedTargetMemory; private float targetValue; @@ -92,8 +113,17 @@ namespace Barotrauma private float avoidTimer; private float observeTimer; private float sweepTimer; - - public bool StayInsideLevel = true; + private float circleRotation; + private float circleDir; + private bool inverseDir; + private bool breakCircling; + private float circleRotationSpeed; + private Vector2 circleOffset; + private float circleFallbackDistance; + private float strikeTimer; + private float aggressionIntensity; + private CirclePhase CirclePhase; + private float currentAttackIntensity; private readonly IEnumerable myBodies; @@ -159,6 +189,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 +203,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 +222,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; @@ -198,6 +232,19 @@ namespace Barotrauma MTRandom random = new MTRandom(ToolBox.StringToInt(seed)); XElement aiElement = aiElements.Count == 1 ? aiElements[0] : ToolBox.SelectWeightedRandom(aiElements, aiCommonness, random); foreach (XElement subElement in aiElement.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "chooserandom": + LoadSubElement(subElement.Elements().GetRandom(random)); + break; + default: + LoadSubElement(subElement); + break; + } + } + + void LoadSubElement(XElement subElement) { switch (subElement.Name.ToString().ToLowerInvariant()) { @@ -216,7 +263,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; @@ -225,11 +272,28 @@ namespace Barotrauma colliderLength = size.Y; requiredHoleCount = (int)Math.Ceiling(ConvertUnits.ToDisplayUnits(colliderWidth) / Structure.WallSectionSize); - avoidLookAheadDistance = Math.Max(colliderWidth * 3, 1.5f); + avoidLookAheadDistance = Math.Max(Math.Max(colliderWidth, colliderLength) * 3, 1.5f); 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) @@ -308,7 +372,7 @@ namespace Barotrauma } private float movementMargin; - + public override void Update(float deltaTime) { if (DisableEnemyAI) { return; } @@ -328,14 +392,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; } @@ -376,7 +449,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 @@ -395,7 +468,7 @@ namespace Barotrauma UpdateTargets(Character, out targetingParams); if (!IsLatchedOnSub) { - UpdateWallTarget(); + UpdateWallTarget(requiredHoleCount); } updateTargetsTimer = updateTargetsInterval * Rand.Range(0.75f, 1.25f); if (SelectedAiTarget == null) @@ -410,21 +483,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; @@ -506,12 +606,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.DamageThreshold; + } + 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); @@ -609,8 +721,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); } } @@ -678,6 +790,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(); @@ -694,35 +808,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) { @@ -747,36 +888,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 { @@ -847,49 +977,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) @@ -919,12 +1009,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) @@ -1106,30 +1190,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; @@ -1189,6 +1277,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) @@ -1238,7 +1336,7 @@ namespace Barotrauma } } } - else if (!IsCoolDownRunning) + else if (!IsAttackRunning && !IsCoolDownRunning) { // If not, reset the attacking limb, if the cooldown is not running // Don't use the property, because we don't want cancel reversing, if we are reversing. @@ -1290,26 +1388,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) { @@ -1325,70 +1424,239 @@ namespace Barotrauma } else { - if (selectedTargetingParams.SweepDistance > 0) + switch (selectedTargetingParams.AttackPattern) { - Vector2 toTarget = attackWorldPos - WorldPosition; - if (distance <= 0) - { - distance = toTarget.Length(); - } - float amplitude = MathHelper.Lerp(0, selectedTargetingParams.SweepStrength, MathUtils.InverseLerp(selectedTargetingParams.SweepDistance, 0, distance)); - if (amplitude > 0) - { - sweepTimer += deltaTime * selectedTargetingParams.SweepSpeed; - float sin = (float)Math.Sin(sweepTimer) * amplitude; - steerPos = MathUtils.RotatePointAroundTarget(attackSimPos, SimPosition, MathHelper.ToDegrees(sin)); - } - else - { - sweepTimer = Rand.Range(-1000, 1000) * selectedTargetingParams.SweepSpeed; - } + case AttackPattern.Sweep: + if (selectedTargetingParams.SweepDistance > 0) + { + if (distance <= 0) + { + distance = (attackWorldPos - WorldPosition).Length(); + } + float amplitude = MathHelper.Lerp(0, selectedTargetingParams.SweepStrength, MathUtils.InverseLerp(selectedTargetingParams.SweepDistance, 0, distance)); + if (amplitude > 0) + { + sweepTimer += deltaTime * selectedTargetingParams.SweepSpeed; + float sin = (float)Math.Sin(sweepTimer) * amplitude; + steerPos = MathUtils.RotatePointAroundTarget(attackSimPos, SimPosition, sin); + } + else + { + sweepTimer = Rand.Range(-1000, 1000) * selectedTargetingParams.SweepSpeed; + } + } + break; + case AttackPattern.Circle: + if (IsCoolDownRunning) { break; } + if (IsAttackRunning) { break; } + if (selectedTargetingParams == null) { break; } + switch (CirclePhase) + { + case CirclePhase.Start: + currentAttackIntensity = MathUtils.InverseLerp(AIParams.StartAggression, AIParams.MaxAggression, aggressionIntensity * Rand.Range(0.9f, 1.1f)); + inverseDir = false; + circleDir = GetDirFromHeadingInRadius(); + circleRotation = 0; + strikeTimer = 0; + blockCheckTimer = 0; + breakCircling = false; + float minRotationSpeed = 0.01f * selectedTargetingParams.CircleRotationSpeed; + float maxRotationSpeed = 0.5f * selectedTargetingParams.CircleRotationSpeed; + float minFallBackDistance = selectedTargetingParams.CircleStartDistance * 0.5f; + float maxFallBackDistance = selectedTargetingParams.CircleStartDistance; + // The lower the rotation speed, the slower the progression. Also the distance to the target stays longer. + // So basically if the value is higher, the creature will strike the sub more quickly and with more precision. + circleRotationSpeed = MathHelper.Lerp(minRotationSpeed, maxRotationSpeed, currentAttackIntensity * Rand.Range(0.9f, 1.1f)); + circleFallbackDistance = MathHelper.Lerp(maxFallBackDistance, minFallBackDistance, currentAttackIntensity * Rand.Range(0.9f, 1.1f)); + circleOffset = Rand.Vector(MathHelper.Lerp(selectedTargetingParams.CircleMaxRandomOffset, 0, currentAttackIntensity * Rand.Range(0.9f, 1.1f))); + canAttack = false; + aggressionIntensity = Math.Clamp(aggressionIntensity, AIParams.StartAggression, AIParams.MaxAggression); + CirclePhase = Vector2.DistanceSquared(WorldPosition, attackWorldPos) > MathUtils.Pow2(circleFallbackDistance) ? CirclePhase.CloseIn : CirclePhase.FallBack; + break; + case CirclePhase.CloseIn: + var sub = SelectedAiTarget.Entity?.Submarine; + if (sub == null) + { + CirclePhase = CirclePhase.Start; + break; + } + if (AttackingLimb != null && distance > 0 && distance < AttackingLimb.attack.Range * GetStrikeDistanceMultiplier(sub.Velocity)) + { + strikeTimer = AttackingLimb.attack.CoolDown; + CirclePhase = CirclePhase.Strike; + } + else if (!breakCircling && Vector2.DistanceSquared(WorldPosition, attackWorldPos) <= MathUtils.Pow2(circleFallbackDistance - 1000) && + sub.Velocity.LengthSquared() <= MathUtils.Pow2(GetTargetMaxSpeed())) + { + CirclePhase = CirclePhase.Advance; + } + canAttack = false; + break; + case CirclePhase.FallBack: + bool isBlocked = !UpdateFallBack(attackWorldPos, deltaTime, followThrough: false, checkBlocking: true); + if (isBlocked || Vector2.DistanceSquared(WorldPosition, attackWorldPos) > MathUtils.Pow2(circleFallbackDistance)) + { + CirclePhase = CirclePhase.Advance; + break; + } + return; + case CirclePhase.Advance: + var targetSub = SelectedAiTarget.Entity?.Submarine; + if (targetSub == null) + { + CirclePhase = CirclePhase.Start; + break; + } + Vector2 subSpeed = targetSub.Velocity; + float requiredDistMultiplier = 1; + // If the target sub is moving fast, just steer towards the target until close enough to strike + if (breakCircling || subSpeed.LengthSquared() > MathUtils.Pow2(GetTargetMaxSpeed()) || distance > selectedTargetingParams.CircleStartDistance + 1000) + { + CirclePhase = CirclePhase.CloseIn; + } + else + { + circleRotation += deltaTime * circleRotationSpeed * circleDir; + if (circleRotation < -360) + { + circleRotation += 360; + } + else if (circleRotation > 360) + { + circleRotation -= 360; + } + Vector2 targetPos = attackSimPos + circleOffset; + if (Vector2.DistanceSquared(SimPosition, targetPos) < 100) + { + // Too close to the target point + // When the offset position is outside of the sub it happens that the creature sometimes reaches the target point, + // which makes it continue circling around the point (as supposed) + // But when there is some offset and the offset is too near, this is not what we want. + if (targetSub.Borders.ContainsWorld(attackWorldPos + ConvertUnits.ToDisplayUnits(circleOffset))) + { + CirclePhase = CirclePhase.Strike; + strikeTimer = AttackingLimb.attack.CoolDown; + } + else + { + CirclePhase = CirclePhase.Start; + } + break; + } + steerPos = MathUtils.RotatePointAroundTarget(SimPosition, targetPos, circleRotation); + requiredDistMultiplier = GetStrikeDistanceMultiplier(subSpeed); + if (IsBlocked(deltaTime, steerPos)) + { + if (!inverseDir) + { + // First try changing the direction + circleDir = -circleDir; + inverseDir = true; + } + else if (circleRotationSpeed < 1) + { + // Then try increasing the rotation speed to change the movement curve + circleRotationSpeed *= 1.1f; + } + else if (circleOffset.LengthSquared() > 0.1f) + { + // Then try removing the offset + circleOffset = Vector2.Zero; + } + else + { + // If we still fail, just steer towards the target + breakCircling = true; + } + } + } + if (AttackingLimb != null && distance > 0 && distance < AttackingLimb.attack.Range * requiredDistMultiplier && IsFacing(margin: MathHelper.Lerp(0.5f, 0.9f, currentAttackIntensity))) + { + strikeTimer = AttackingLimb.attack.CoolDown; + CirclePhase = CirclePhase.Strike; + } + canAttack = false; + break; + case CirclePhase.Strike: + strikeTimer -= deltaTime; + // just continue the movement forward to make it possible to evade the attack + steerPos = SimPosition + Steering; + if (strikeTimer <= 0) + { + CirclePhase = CirclePhase.Start; + aggressionIntensity += AIParams.AggressionCumulation; + } + break; + } + break; + + bool IsFacing(float margin) + { + float offset = steeringLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2; + Vector2 forward = VectorExtensions.Forward(steeringLimb.body.TransformedRotation - offset * Character.AnimController.Dir); + return Vector2.Dot(Vector2.Normalize(attackWorldPos - WorldPosition), forward) > margin; + } + + float GetStrikeDistanceMultiplier(Vector2 subSpeed) + { + float requiredDistMultiplier = 2; + bool isHeading = Steering != null && Vector2.Dot(Vector2.Normalize(attackWorldPos - WorldPosition), Vector2.Normalize(Steering)) > 0.9f; + if (isHeading) + { + requiredDistMultiplier = selectedTargetingParams.CircleStrikeDistanceMultiplier; + float subSpeedHorizontal = Math.Abs(subSpeed.X); + if (subSpeedHorizontal > 1) + { + // Reduce the required distance if the target is moving. + requiredDistMultiplier -= MathHelper.Lerp(0, Math.Max(selectedTargetingParams.CircleStrikeDistanceMultiplier - 1, 1), Math.Clamp(subSpeedHorizontal / 10, 0, 1)); + if (requiredDistMultiplier < 2) + { + requiredDistMultiplier = 2; + } + } + } + return requiredDistMultiplier; + } + + float GetDirFromHeadingInRadius() + { + Vector2 heading = VectorExtensions.Forward(Character.AnimController.Collider.Rotation); + float angle = MathUtils.VectorToAngle(heading); + return angle > MathHelper.Pi || angle < -MathHelper.Pi ? -1 : 1; + } + + float GetTargetMaxSpeed() => Character.ApplyTemporarySpeedLimits(Character.AnimController.CurrentSwimParams.MovementSpeed * 0.3f); } SteeringManager.SteeringSeek(steerPos, 10); - SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15); + if (SelectedAiTarget?.Entity is Character || distance == 0 || distance > ConvertUnits.ToDisplayUnits(avoidLookAheadDistance * 2)) + { + SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 30); + } } } if (canAttack) { - if (!UpdateLimbAttack(deltaTime, AttackingLimb, attackSimPos, distance, attackTargetLimb)) + 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)) + { + CirclePhase = CirclePhase.Start; + } + else { IgnoreTarget(SelectedAiTarget); } } } - 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) @@ -1458,97 +1726,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) - { - 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); @@ -1581,7 +1758,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) { @@ -1639,11 +1816,11 @@ 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) + if (State == AIState.Attack && !IsAttackRunning && !IsCoolDownRunning) { - // Don't retaliate or escape while performing an attack + // Don't retaliate or escape while performing an attack/under cooldown retaliate = false; avoidGunFire = false; } @@ -1664,7 +1841,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); @@ -1678,6 +1855,9 @@ namespace Barotrauma private bool UpdateLimbAttack(float deltaTime, Limb attackingLimb, Vector2 attackSimPos, float distance = -1, Limb targetLimb = null) { if (SelectedAiTarget?.Entity == null) { return false; } + + ActiveAttack = attackingLimb?.attack; + if (wallTarget != null) { // If the selected target is not the wall target, make the wall target the selected target. @@ -1685,6 +1865,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; @@ -1709,8 +1890,22 @@ namespace Barotrauma return false; } + private readonly float blockCheckInterval = 0.1f; + private float blockCheckTimer; + private bool isBlocked; + private bool IsBlocked(float deltaTime, Vector2 steerPos, Category collisionCategory = Physics.CollisionLevel) + { + blockCheckTimer -= deltaTime; + if (blockCheckTimer <= 0) + { + blockCheckTimer = blockCheckInterval; + isBlocked = Submarine.PickBodies(SimPosition, steerPos, collisionCategory: collisionCategory).Any(); + } + return isBlocked; + } + private Vector2? attackVector = null; - private void UpdateFallBack(Vector2 attackWorldPos, float deltaTime, bool followThrough) + private bool UpdateFallBack(Vector2 attackWorldPos, float deltaTime, bool followThrough, bool checkBlocking = false) { if (attackVector == null) { @@ -1727,6 +1922,11 @@ namespace Barotrauma { SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15); } + if (checkBlocking) + { + return !IsBlocked(deltaTime, SimPosition + attackDir * (avoidLookAheadDistance / 2)); + } + return true; } #endregion @@ -1942,8 +2142,13 @@ namespace Barotrauma { // Ignore all structures and items inside wrecks if (aiTarget.Entity.Submarine != null && aiTarget.Entity.Submarine.Info.IsWreck) { continue; } - // Ignore the target if it's a room and the character is already inside a sub - if (character.CurrentHull != null && aiTarget.Entity is Hull) { continue; } + if (aiTarget.Entity is Hull hull) + { + // Ignore the target if it's a room and the character is already inside a sub + if (character.CurrentHull != null) { continue; } + // Ignore ruins + if (hull.Submarine == null) { continue; } + } Door door = null; if (aiTarget.Entity is Item item) @@ -1952,7 +2157,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; @@ -2100,16 +2305,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) @@ -2135,13 +2338,15 @@ 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.IgnoreIfNotInSameSub && aiTarget.Entity.Submarine != Character.Submarine) { continue; } if (targetParams.State == AIState.Observe || targetParams.State == AIState.Eat) { if (targetCharacter != null && targetCharacter.Submarine != Character.Submarine) { - // Don't allow to target characters that are inside a different submarine / outside when we are inside. + // Never allow observing or eating characters that are inside a different submarine / outside when we are inside. continue; } } @@ -2197,6 +2402,20 @@ namespace Barotrauma // Inside the sub, treat objects that are up or down, as they were farther away. dist *= 3; } + + if (targetParams.AttackPattern == AttackPattern.Circle) + { + if (Character.Submarine == null && aiTarget.Entity?.Submarine != null) + { + if (Submarine.MainSubs.Contains(aiTarget.Entity.Submarine)) + { + // Prioritize targets that are near the horizontal center of the sub + float horizontalDistanceToSubCenter = Math.Abs(aiTarget.WorldPosition.X - aiTarget.Entity.Submarine.WorldPosition.X); + dist *= MathHelper.Lerp(1f, 5f, MathUtils.InverseLerp(0, 10000, horizontalDistanceToSubCenter)); + } + } + } + valueModifier *= targetMemory.Priority / (float)Math.Sqrt(dist); if (valueModifier > targetValue) @@ -2248,7 +2467,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) @@ -2271,7 +2490,7 @@ namespace Barotrauma newTarget = aiTarget; selectedTargetMemory = targetMemory; targetValue = valueModifier; - targetingParams = GetTargetParams(targetingTag); + targetingParams = targetParams; } } @@ -2285,7 +2504,7 @@ namespace Barotrauma wall = wallTarget?.Structure; } // The target is not a wall or it's not the same as we are attached to -> release - bool releaseTarget = wall == null || !wall.Bodies.Contains(LatchOntoAI.AttachJoints[0].BodyB); + bool releaseTarget = wall == null || (!wall.Bodies.Contains(LatchOntoAI.AttachJoints[0].BodyB) && wall.Submarine?.PhysicsBody?.FarseerBody != LatchOntoAI.AttachJoints[0].BodyB); if (!releaseTarget) { for (int i = 0; i < wall.Sections.Length; i++) @@ -2310,6 +2529,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 && w.Submarine == SelectedAiTarget.Entity?.Submarine || + closestBody.UserData is Item i && i.Submarine != null && i.Submarine == SelectedAiTarget.Entity?.Submarine) + { + // Cannot reach the target, because it's blocked by a disabled wall or a door + 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)) @@ -2524,6 +2886,7 @@ namespace Barotrauma { SetStateResetTimer(); } + blockCheckTimer = 0; } private void SetStateResetTimer() => stateResetTimer = stateResetCooldown * Rand.Range(0.75f, 1.25f); @@ -2579,23 +2942,37 @@ namespace Barotrauma private float returnTimer; private void SteerInsideLevel(float deltaTime) { - if (SteeringManager is IndoorsSteeringManager || !StayInsideLevel) { return; } + if (State == AIState.Attack) { return; } + if (SteeringManager is IndoorsSteeringManager) { return; } if (Level.Loaded == null) { return; } Point levelSize = Level.Loaded.Size; float returnTime = 10; - if (WorldPosition.Y < 0) + if (AIParams.AvoidAbyss) { - // Too far down - returnTimer = returnTime * Rand.Range(0.75f, 1.25f); - returnDir = Vector2.UnitY; + if (WorldPosition.Y < Level.Loaded.AbyssStart) + { + // Too far down + returnTimer = returnTime * Rand.Range(0.75f, 1.25f); + returnDir = Vector2.UnitY; + } } - if (WorldPosition.X < 0) + else if (AIParams.StayInAbyss) + { + if (WorldPosition.Y > Level.Loaded.AbyssStart) + { + // Too far up + returnTimer = returnTime * Rand.Range(0.75f, 1.25f); + returnDir = -Vector2.UnitY; + } + } + float margin = AIParams.AvoidAbyss ? 0 : 30000; + if (WorldPosition.X < margin) { // Too far left returnTimer = returnTime * Rand.Range(0.75f, 1.25f); returnDir = Vector2.UnitX; } - if (WorldPosition.X > levelSize.X) + if (WorldPosition.X > levelSize.X + margin) { // Too far right returnTimer = returnTime * Rand.Range(0.75f, 1.25f); @@ -2605,32 +2982,43 @@ namespace Barotrauma { returnTimer -= deltaTime; SteeringManager.Reset(); - SteeringManager.SteeringManual(deltaTime, returnDir * 2); + SteeringManager.SteeringManual(deltaTime, returnDir * 10); + SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, 15); } } - 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 8318de61d..dade0434f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -10,6 +10,7 @@ namespace Barotrauma { partial class HumanAIController : AIController { + public static bool debugai; public static bool DisableCrewAI; private readonly AIObjectiveManager objectiveManager; @@ -28,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(); @@ -37,6 +40,7 @@ namespace Barotrauma private float respondToAttackTimer; private const float RespondToAttackInterval = 1.0f; + private bool wasConscious; private bool freezeAI; @@ -48,6 +52,30 @@ namespace Barotrauma private readonly float obstacleRaycastInterval = 1; private float obstacleRaycastTimer; + private readonly float enemyCheckInterval = 0.2f; + private readonly float enemySpotDistanceOutside = 1500; + private readonly float enemySpotDistanceInside = 1000; + private float enemycheckTimer; + + /// + /// How far other characters can hear reports done by this character (e.g. reports for fires, intruders). Defaults to infinity. + /// + public float ReportRange { get; set; } = float.PositiveInfinity; + + private float _aimSpeed = 1; + public float AimSpeed + { + get { return _aimSpeed; } + set { _aimSpeed = Math.Max(value, 0.01f); } + } + + private float _aimAccuracy = 1; + public float AimAccuracy + { + get { return _aimAccuracy; } + set { _aimAccuracy = Math.Clamp(value, 0f, 1f); } + } + /// /// List of previous attacks done to this character /// @@ -58,28 +86,11 @@ namespace Barotrauma public IndoorsSteeringManager PathSteering => insideSteering as IndoorsSteeringManager; public HumanoidAnimController AnimController => Character.AnimController as HumanoidAnimController; - public override AIObjectiveManager ObjectiveManager - { - get { return objectiveManager; } - } - - public Order CurrentOrder - { - get; - private set; - } - - public string CurrentOrderOption - { - get; - private set; - } + public AIObjectiveManager ObjectiveManager => objectiveManager; 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 { @@ -121,31 +132,12 @@ namespace Barotrauma objectiveManager = new AIObjectiveManager(c); reactTimer = GetReactionTime(); sortTimer = Rand.Range(0f, sortObjectiveInterval); - InitProjSpecific(); } - partial void InitProjSpecific(); - public override void Update(float deltaTime) { 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) { @@ -153,6 +145,8 @@ namespace Barotrauma } if (isIncapacitated) { return; } + wasConscious = true; + respondToAttackTimer -= deltaTime; if (respondToAttackTimer <= 0.0f) { @@ -184,25 +178,64 @@ 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) + if (Character.Submarine == null) { - obstacleRaycastTimer -= deltaTime; - if (obstacleRaycastTimer <= 0) + if (hasValidPath) { - obstacleRaycastTimer = obstacleRaycastInterval; - // Swimming outside and using the path finder -> check that the path is not blocked with anything (the path finder doesn't know about other subs). - foreach (var connectedSub in Submarine.MainSub.GetConnectedSubs()) + obstacleRaycastTimer -= deltaTime; + if (obstacleRaycastTimer <= 0) { - if (connectedSub == Submarine.MainSub) { continue; } - Vector2 rayStart = SimPosition - connectedSub.SimPosition; - Vector2 dir = PathSteering.CurrentPath.CurrentNode.WorldPosition - WorldPosition; - Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 5); - if (Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true) != null) + obstacleRaycastTimer = obstacleRaycastInterval; + // Swimming outside and using the path finder -> check that the path is not blocked with anything (the path finder doesn't know about other subs). + foreach (var connectedSub in Submarine.MainSub.GetConnectedSubs()) { - PathSteering.CurrentPath.Unreachable = true; - break; + if (connectedSub == Submarine.MainSub) { continue; } + Vector2 rayStart = SimPosition - connectedSub.SimPosition; + Vector2 dir = PathSteering.CurrentPath.CurrentNode.WorldPosition - WorldPosition; + Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 5); + if (Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true) != null) + { + PathSteering.CurrentPath.Unreachable = true; + break; + } + } + } + } + } + if (Character.Submarine == null || !IsOnFriendlyTeam(Character.TeamID, Character.Submarine.TeamID)) + { + // Spot enemies while staying outside or inside an enemy ship. + enemycheckTimer -= deltaTime; + if (enemycheckTimer < 0) + { + enemycheckTimer = enemyCheckInterval * Rand.Range(0.75f, 1.25f); + if (!objectiveManager.IsCurrentObjective()) + { + float closestDistance = 0; + Character closestEnemy = null; + foreach (Character c in Character.CharacterList) + { + if (c.Submarine != Character.Submarine) { continue; } + if (IsFriendly(c)) { continue; } + Vector2 toTarget = c.WorldPosition - WorldPosition; + float dist = toTarget.LengthSquared(); + float maxDistance = Character.Submarine == null ? enemySpotDistanceOutside : enemySpotDistanceInside; + if (dist > maxDistance * maxDistance) { continue; } + Vector2 forward = VectorExtensions.Forward(Character.AnimController.Collider.Rotation); + forward.X *= Character.AnimController.Dir; + if (Vector2.Dot(toTarget, forward) < 0.2f) { continue; } + if (!Character.CanSeeCharacter(c)) { continue; } + if (dist < closestDistance || closestEnemy == null) + { + closestEnemy = c; + closestDistance = dist; + } + } + if (closestEnemy != null) + { + AddCombatObjective(AIObjectiveCombat.CombatMode.Defensive, closestEnemy); } } } @@ -255,9 +288,9 @@ 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. + // Outpost npcs don't inform each other about threats, like crew members do. VisibleHulls.ForEach(h => RefreshHullSafety(h)); } else @@ -384,8 +417,11 @@ namespace Barotrauma { if (findItemState != FindItemState.OtherItem) { - if (ObjectiveManager.GetActiveObjective() is AIObjectiveGoTo gotoObjective && NeedsDivingGearOnPath(gotoObjective)) + var decontain = ObjectiveManager.GetActiveObjectives().LastOrDefault(); + if (decontain != null && decontain.TargetItem != null && decontain.TargetItem.HasTag(AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR) && + 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; } } @@ -398,18 +434,22 @@ namespace Barotrauma // Diving gear if (oxygenLow || findItemState != FindItemState.OtherItem) { - if (!NeedsDivingGear(Character.CurrentHull, out bool needsSuit) || !needsSuit || oxygenLow) + bool needsGear = NeedsDivingGear(Character.CurrentHull, out _); + if (!needsGear || oxygenLow) { - bool shouldKeepTheGearOn = Character.AnimController.HeadInWater - || Character.Submarine.TeamID != Character.TeamID - || ObjectiveManager.IsCurrentObjective() - || ObjectiveManager.CurrentOrder is AIObjectiveGoTo goTo && goTo.Target == Character // wait order - || ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn); + bool shouldKeepTheGearOn = + Character.AnimController.InWater || + Character.AnimController.HeadInWater || + Character.CurrentHull == null || + Character.Submarine.TeamID != Character.TeamID || + ObjectiveManager.IsCurrentObjective() || + ObjectiveManager.CurrentOrder is AIObjectiveGoTo goTo && goTo.Target == Character || // wait order + ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn); if (oxygenLow && Character.CurrentHull.Oxygen > 0) { shouldKeepTheGearOn = false; } - else if (Character.CurrentHull.Oxygen < CharacterHealth.LowOxygenThreshold) + else if (Character.CurrentHull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 10) { shouldKeepTheGearOn = true; } @@ -553,39 +593,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); } } } @@ -597,7 +635,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); @@ -622,7 +660,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; @@ -649,18 +687,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, @@ -676,23 +702,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 { @@ -744,6 +770,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) @@ -793,17 +831,17 @@ 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"), minDurationBetweenSimilar: 60.0f); } - else if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime)) + else if (Character.IsOnPlayerTeam && GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime)) { Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName, givingOrderToSelf: false), ChatMessageType.Order); #if SERVER - GameMain.Server.SendOrderChatMessage(new OrderChatMessage(newOrder, "", targetHull, null, Character)); + GameMain.Server.SendOrderChatMessage(new OrderChatMessage(newOrder, "", CharacterInfo.HighestManualOrderPriority, targetHull, null, Character)); #endif } } @@ -822,6 +860,8 @@ namespace Barotrauma private void UpdateSpeaking() { + if (!Character.IsOnPlayerTeam) { return; } + if (Character.Oxygen < 20.0f) { Character.Speak(TextManager.Get("DialogLowOxygen"), null, Rand.Range(0.5f, 5.0f), "lowoxygen", 30.0f); @@ -832,14 +872,21 @@ 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); } } public override void OnAttacked(Character attacker, AttackResult attackResult) { + // The attack incapacitated/killed the character: respond immediately to trigger nearby characters because the update loop no longer runs + if (wasConscious && (Character.IsIncapacitated || Character.Stun > 0.0f)) + { + RespondToAttack(attacker, attackResult); + wasConscious = false; + return; + } if (Character.IsDead) { return; } if (attacker == null || Character.IsPlayer) { @@ -883,15 +930,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. @@ -902,7 +940,7 @@ namespace Barotrauma } if (attacker == null || attacker.IsDead || attacker.Removed) { - // Don't react on the damage if there's no attacker. + // Don't react to the damage if there's no attacker. // We might consider launching the retreat combat objective in some cases, so that the bot does not just stand somewhere getting damaged and dying. // But fires and enemies should already be handled by the FindSafetyObjective. return; @@ -910,12 +948,17 @@ namespace Barotrauma //if (Character.LastDamageSource == null) { return; } //AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, Rand.Range(0.5f, 1f, Rand.RandSync.Unsynced)); } - else if (realDamage <= 0 && (attacker.IsBot || attacker.TeamID == Character.TeamID)) + if (realDamage <= 0 && (attacker.IsBot || attacker.TeamID == Character.TeamID)) { - // Don't react on damage that is entirely based on karma penalties (medics, poisons etc), unless applier is player + // Don't react to damage that is entirely based on karma penalties (medics, poisons etc), unless applier is player return; } - else if (IsFriendly(attacker)) + if (attacker.Submarine == null && Character.Submarine != null) + { + // Don't react to attackers that are outside of the sub (e.g. AoE attacks) + return; + } + if (IsFriendly(attacker)) { if (attacker.AnimController.Anim == Barotrauma.AnimController.Animation.CPR && attacker.SelectedCharacter == Character) { @@ -928,7 +971,7 @@ namespace Barotrauma { if (cumulativeDamage > 1) { - // Don't retaliate on damage done by human ai, because we know it's accidental + // Don't retaliate on damage done by friendly NPC, because we know it's accidental AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker); } } @@ -938,49 +981,29 @@ namespace Barotrauma // Inform other NPCs if (cumulativeDamage > 1) { - 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.AIController is HumanAIController otherHumanAI) || - otherCharacter.IsInstigator) - { - continue; - } - if (!otherHumanAI.IsFriendly(Character)) { continue; } - bool isWitnessing = otherHumanAI.VisibleHulls.Contains(Character.CurrentHull) || otherHumanAI.VisibleHulls.Contains(attacker.CurrentHull); - if (otherCharacter.IsSecurity) - { - // Alert all the security officers magically - float delay = isWitnessing ? GetReactionTime() * 2 : Rand.Range(2.0f, 5.0f, Rand.RandSync.Unsynced); - otherHumanAI.AddCombatObjective(DetermineCombatMode(otherCharacter, cumulativeDamage), attacker, delay); - } - else if (isWitnessing) - { - var mode = Character.CombatAction != null ? Character.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat; - // Other witnesses retreat to safety - otherHumanAI.AddCombatObjective(mode, attacker, GetReactionTime()); - } - } + InformOtherNPCs(cumulativeDamage); } if (Character.IsBot) { if (ObjectiveManager.CurrentObjective is AIObjectiveFightIntruders) { return; } - if (Character.IsSecurity) + if (attacker.IsPlayer) { - if (attacker.TeamID != Character.TeamID && cumulativeDamage > 1 || cumulativeDamage > 10) + if (Character.IsSecurity) { - Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityarrest"), null, 0.50f, "attackedbyfriendlysecurityarrest", minDurationBetweenSimilar: 30.0f); + if (attacker.TeamID != Character.TeamID && cumulativeDamage > 1 || cumulativeDamage > 10) + { + Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityarrest"), null, 0.50f, "attackedbyfriendlysecurityarrest", minDurationBetweenSimilar: 30.0f); + } + else + { + Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityresponse"), null, 0.50f, "attackedbyfriendlysecurityresponse", minDurationBetweenSimilar: 30.0f); + } } - else + else if (!Character.IsInstigator && cumulativeDamage > 1) { - Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityresponse"), null, 0.50f, "attackedbyfriendlysecurityresponse", minDurationBetweenSimilar: 30.0f); + Character.Speak(TextManager.Get("DialogAttackedByFriendly"), null, 0.50f, "attackedbyfriendly", minDurationBetweenSimilar: 30.0f); } } - else if (!Character.IsInstigator && cumulativeDamage > 1) - { - Character.Speak(TextManager.Get("DialogAttackedByFriendly"), null, 0.50f, "attackedbyfriendly", minDurationBetweenSimilar: 30.0f); - } if (cumulativeDamage > 1 && attacker.TeamID != Character.TeamID) { // If the attacker is using a low damage and high frequency weapon like a repair tool, we shouldn't use any delay. @@ -993,7 +1016,7 @@ namespace Barotrauma { cumulativeDamage = 100; } - // Don't react on minor (accidental) dmg done by characters that are in the same team + // Don't react to minor (accidental) dmg done by characters that are in the same team if (cumulativeDamage < 10) { if (!Character.IsSecurity && cumulativeDamage > 1) @@ -1009,13 +1032,36 @@ namespace Barotrauma } } } - else if (Character.IsBot) + else { // Non-friendly - AddCombatObjective(DetermineCombatMode(Character, cumulativeDamage: realDamage), attacker); + InformOtherNPCs(GetDamageDoneByAttacker(attacker)); + if (Character.IsBot) + { + AddCombatObjective(DetermineCombatMode(Character, cumulativeDamage: realDamage), attacker); + } } - AIObjectiveCombat.CombatMode DetermineCombatMode(Character c, float cumulativeDamage, float dmgThreshold = 10, bool allowOffensive = true) + void InformOtherNPCs(float cumulativeDamage) + { + foreach (Character otherCharacter in Character.CharacterList) + { + if (otherCharacter == Character || otherCharacter.IsDead || otherCharacter.IsUnconscious || otherCharacter.Removed) { continue; } + if (otherCharacter.Submarine != Character.Submarine) { continue; } + if (otherCharacter.Submarine != attacker.Submarine) { continue; } + if (otherCharacter.Info?.Job == null || otherCharacter.IsInstigator) { continue; } + if (otherCharacter.IsPlayer) { continue; } + if (!(otherCharacter.AIController is HumanAIController otherHumanAI)) { continue; } + if (!otherHumanAI.IsFriendly(Character)) { continue; } + bool isWitnessing = otherHumanAI.VisibleHulls.Contains(Character.CurrentHull) || otherHumanAI.VisibleHulls.Contains(attacker.CurrentHull); + if (!isWitnessing && !CheckReportRange(Character, otherCharacter, ReportRange)) { continue; } + var combatMode = DetermineCombatMode(otherCharacter, cumulativeDamage, isWitnessing); + float delay = isWitnessing ? GetReactionTime() : Rand.Range(2.0f, 5.0f, Rand.RandSync.Unsynced); + otherHumanAI.AddCombatObjective(combatMode, attacker, delay); + } + } + + AIObjectiveCombat.CombatMode DetermineCombatMode(Character c, float cumulativeDamage, bool isWitnessing = false, float dmgThreshold = 10, bool allowOffensive = true) { if (!IsFriendly(attacker)) { @@ -1028,12 +1074,16 @@ namespace Barotrauma { return AIObjectiveCombat.CombatMode.None; } - if (Character.IsInstigator && attacker.IsPlayer) + else if (isWitnessing && Character.CombatAction != null && !c.IsSecurity) + { + return Character.CombatAction.WitnessReaction; + } + else if (Character.IsInstigator && attacker.IsPlayer) { // 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) { @@ -1066,15 +1116,16 @@ 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 target, 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; } - if (ObjectiveManager.CurrentObjective is AIObjectiveCombat combatObjective) + if (Character.IsDead || Character.IsIncapacitated || Character.Removed) { return; } + if (!Character.IsBot) { return; } + if (ObjectiveManager.Objectives.FirstOrDefault(o => o is AIObjectiveCombat) is AIObjectiveCombat combatObjective) { // Don't replace offensive mode with something else if (combatObjective.Mode == AIObjectiveCombat.CombatMode.Offensive && mode != AIObjectiveCombat.CombatMode.Offensive) { return; } - if (combatObjective.Mode != mode || combatObjective.Enemy != attacker || (combatObjective.Enemy == null && attacker == null)) + if (combatObjective.Mode != mode || combatObjective.Enemy != target || (combatObjective.Enemy == null && target == null)) { // Replace the old objective with the new. ObjectiveManager.Objectives.Remove(combatObjective); @@ -1095,9 +1146,12 @@ namespace Barotrauma AIObjectiveCombat CreateCombatObjective() { - var objective = new AIObjectiveCombat(Character, attacker, mode, objectiveManager) + var objective = new AIObjectiveCombat(Character, target, mode, objectiveManager) { - HoldPosition = Character.Info?.Job?.Prefab.Identifier == "watchman", + HoldPosition = + Character.Info?.Job?.Prefab.Identifier == "watchman" || + Character.CurrentHull == null || + Character.IsOnPlayerTeam && ObjectiveManager.GetActiveObjective()?.Target is Character followTarget && followTarget.IsPlayer, abortCondition = abortCondition, allowHoldFire = allowHoldFire, }; @@ -1105,49 +1159,28 @@ 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) + + public void SetOrder(Order order, string option, int priority, 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, priority, orderGiver, speak); + } + + public void SetForcedOrder(Order order, string option, Character orderGiver) + { + var objective = ObjectiveManager.CreateObjective(order, option, orderGiver, false); + ObjectiveManager.SetForcedOrder(objective); + } + + public void ClearForcedOrder() + { + ObjectiveManager.ClearForcedOrder(); } public override void SelectTarget(AITarget target) @@ -1201,56 +1234,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; @@ -1262,7 +1245,7 @@ namespace Barotrauma needsSuit = true; return true; } - if (hull.WaterPercentage > 60 || hull.OxygenPercentage < CharacterHealth.LowOxygenThreshold) + if (hull.WaterPercentage > 60 || hull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 1) { return true; } @@ -1282,20 +1265,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) @@ -1304,7 +1385,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) { @@ -1473,7 +1554,7 @@ namespace Barotrauma targetAdded = true; } } - }); + }, (caller.AIController as HumanAIController)?.ReportRange ?? float.PositiveInfinity); return targetAdded; } @@ -1483,11 +1564,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; } @@ -1525,7 +1608,7 @@ namespace Barotrauma bool ignoreFire = objectiveManager.CurrentOrder is AIObjectiveExtinguishFires extinguishOrder && extinguishOrder.Priority > 0 || objectiveManager.HasActiveObjective(); bool ignoreWater = HasDivingSuit(character); bool ignoreOxygen = ignoreWater || HasDivingMask(character); - bool ignoreEnemies = ObjectiveManager.IsCurrentObjective(); + bool ignoreEnemies = ObjectiveManager.IsCurrentOrder() || ObjectiveManager.Objectives.Any(o => o is AIObjectiveFightIntruders); float safety = CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies); if (isCurrentHull) { @@ -1538,9 +1621,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; @@ -1565,14 +1649,14 @@ namespace Barotrauma enemyFactor = MathHelper.Lerp(1, 0, MathHelper.Clamp(enemyCount * 0.9f, 0, 1)); } float dangerousItemsFactor = 1f; - foreach (Item item in Item.ItemList.Where(it => it.CurrentHull == hull)) + foreach (Item item in Item.ItemList) { + if (item.CurrentHull != hull) { continue; } if (item.Prefab != null && item.Prefab.IsDangerous) { dangerousItemsFactor = 0; } } - float safety = oxygenFactor * waterFactor * fireFactor * enemyFactor * dangerousItemsFactor; return MathHelper.Clamp(safety * 100, 0, 100); } @@ -1619,12 +1703,12 @@ namespace Barotrauma public static bool IsFriendly(Character me, Character other, bool onlySameTeam = false) { bool sameTeam = me.TeamID == other.TeamID; - bool friendlyTeam = IsOnFriendlyTeam(GameMain.GameSession?.GameMode, me, other); + bool friendlyTeam = IsOnFriendlyTeam(me, other); bool teamGood = sameTeam || friendlyTeam && !onlySameTeam; 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) @@ -1635,18 +1719,29 @@ namespace Barotrauma return true; } - private static bool IsOnFriendlyTeam(GameMode mode, Character me, Character other) + public static bool IsOnFriendlyTeam(CharacterTeamType myTeam, CharacterTeamType otherTeam) { - // Only enemies are in the Team "None" - bool friendlyTeam = me.TeamID != Character.TeamType.None && other.TeamID != Character.TeamType.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) + if (myTeam == otherTeam) { return true; } + + switch (myTeam) { - friendlyTeam = me.TeamID == other.TeamID; + case CharacterTeamType.None: + // Only enemies are in the Team "None" + return false; + case CharacterTeamType.Team1: + case CharacterTeamType.Team2: + // Team1 is only friendly to Team1 and friendly NPCs + return otherTeam == CharacterTeamType.FriendlyNPC; + case CharacterTeamType.FriendlyNPC: + // Friendly NPCs are friendly to both teams + return otherTeam == CharacterTeamType.Team1 || otherTeam == CharacterTeamType.Team2; + default: + return true; } - return friendlyTeam; } + public static bool IsOnFriendlyTeam(Character me, Character other) => IsOnFriendlyTeam(me.TeamID, other.TeamID); + public static bool IsActive(Character other) => other != null && !other.Removed && !other.IsDead && !other.IsUnconscious; public static bool IsTrueForAllCrewMembers(Character character, Func predicate) @@ -1706,18 +1801,31 @@ namespace Barotrauma return count; } - public static void DoForEachCrewMember(Character character, Action action) + public static void DoForEachCrewMember(Character character, Action action, float range = float.PositiveInfinity) { if (character == null) { return; } foreach (var c in Character.CharacterList) { - if (FilterCrewMember(character, c)) + if (FilterCrewMember(character, c) && CheckReportRange(character, c, range)) { action(c.AIController as HumanAIController); } } } + private static bool CheckReportRange(Character character, Character target, float range) + { + if (float.IsPositiveInfinity(range)) { return true; } + if (character.CurrentHull == null || target.CurrentHull == null) + { + return Vector2.DistanceSquared(character.WorldPosition, target.WorldPosition) <= range * range; + } + else + { + return character.CurrentHull.GetApproximateDistance(character.Position, target.Position, target.CurrentHull, range, distanceMultiplierPerClosedDoor: 2) <= range; + } + } + private static bool FilterCrewMember(Character self, Character other) => other != null && !other.IsDead && !other.Removed && other.AIController is HumanAIController humanAi && humanAi.IsFriendly(self); public static bool IsItemOperatedByAnother(Character character, ItemComponent target, out Character operatingCharacter) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs index 3299ec59d..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; } @@ -117,13 +117,13 @@ namespace Barotrauma } /// - /// Seeks the ladder from the current and the next two nodes. + /// Seeks the ladder from the next and next + 1 nodes. /// public Ladder GetNextLadder() { 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; } @@ -294,10 +294,16 @@ namespace Barotrauma bool isDiving = character.AnimController.InWater && character.AnimController.HeadInWater; // Only humanoids can climb ladders bool canClimb = character.AnimController is HumanoidAnimController && !character.LockHands; - var ladders = GetNextLadder(); + Ladder currentLadder = currentPath.CurrentNode.Ladders; + if (currentLadder != null && !currentLadder.Item.IsInteractable(character)) + { + currentLadder = null; + } + Ladder nextLadder = GetNextLadder(); + 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)) { @@ -325,7 +331,6 @@ namespace Barotrauma if (character.IsClimbing && !isDiving) { Vector2 diff = currentPath.CurrentNode.SimPosition - pos; - Ladder nextLadder = GetNextLadder(); bool nextLadderSameAsCurrent = IsNextLadderSameAsCurrent; if (nextLadderSameAsCurrent) { @@ -341,8 +346,7 @@ namespace Barotrauma diff.Y = Math.Max(diff.Y, 1.0f); } // We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative. - float margin = 0.1f; - bool isAboveFloor = heightFromFloor > -margin && heightFromFloor < collider.height * 1.5f; + bool isAboveFloor = heightFromFloor > -0.1f; // If the next waypoint is horizontally far, we don't want to keep holding the ladders if (isAboveFloor && (nextLadder == null || Math.Abs(currentPath.CurrentNode.WorldPosition.X - currentPath.NextNode.WorldPosition.X) > 50)) { @@ -357,7 +361,7 @@ namespace Barotrauma nextLadder.Item.TryInteract(character, false, true); } } - if (nextLadder != null || isAboveFloor) + if (isAboveFloor || nextLadderSameAsCurrent) { currentPath.SkipToNextNode(); } @@ -383,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; @@ -417,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(); } @@ -434,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; } } @@ -620,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) @@ -641,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/LatchOntoAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs index 91bc0a874..9ffdcf361 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs @@ -19,6 +19,7 @@ namespace Barotrauma private Body targetBody; private Vector2 attachSurfaceNormal; private Submarine targetSubmarine; + private readonly Character character; public bool AttachToSub { get; private set; } public bool AttachToWalls { get; private set; } @@ -74,22 +75,24 @@ namespace Barotrauma attachLimb = enemyAI.Character.AnimController.MainLimb; } + character = enemyAI.Character; enemyAI.Character.OnDeath += OnCharacterDeath; } public void SetAttachTarget(Structure wall, Vector2 attachPos, Vector2 attachSurfaceNormal) { + if (wall == null) { return; } + var sub = wall.Submarine; + if (sub == null) { return; } targetWall = wall; - targetBody = wall.Submarine.PhysicsBody.FarseerBody; - targetSubmarine = wall.Submarine; + targetSubmarine = sub; + targetBody = targetSubmarine.PhysicsBody.FarseerBody; this.attachSurfaceNormal = attachSurfaceNormal; wallAttachPos = attachPos; } public void Update(EnemyAIController enemyAI, float deltaTime) { - Character character = enemyAI.Character; - if (character.Submarine != null) { DeattachFromBody(reset: true); @@ -160,12 +163,12 @@ namespace Barotrauma { if (MathUtils.GetLineIntersection(edge.Point1, edge.Point2, character.WorldPosition, cell.Center, out Vector2 intersection)) { - attachSurfaceNormal = edge.GetNormal(cell); - targetBody = cell.Body; Vector2 potentialAttachPos = ConvertUnits.ToSimUnits(intersection); - float distSqr = Vector2.DistanceSquared(character.SimPosition, wallAttachPos); + float distSqr = Vector2.DistanceSquared(character.SimPosition, potentialAttachPos); if (distSqr < closestDist) { + attachSurfaceNormal = edge.GetNormal(cell); + targetBody = cell.Body; wallAttachPos = potentialAttachPos; closestDist = distSqr; } @@ -183,7 +186,7 @@ namespace Barotrauma wallAttachPos = Vector2.Zero; } - if (wallAttachPos == Vector2.Zero) + if (wallAttachPos == Vector2.Zero || targetBody == null) { DeattachFromBody(reset: false); } @@ -194,7 +197,7 @@ namespace Barotrauma if (squaredDistance < targetDistance * targetDistance) { //close enough to a wall -> attach - AttachToBody(character.AnimController.Collider, attachLimb, targetBody, wallAttachPos); + AttachToBody(wallAttachPos); enemyAI.SteeringManager.Reset(); } else @@ -217,7 +220,7 @@ namespace Barotrauma { if (Vector2.DistanceSquared(ConvertUnits.ToDisplayUnits(transformedAttachPos), enemyAI.AttackingLimb.WorldPosition) < enemyAI.AttackingLimb.attack.DamageRange * enemyAI.AttackingLimb.attack.DamageRange) { - AttachToBody(character.AnimController.Collider, attachLimb, targetBody, transformedAttachPos); + AttachToBody(transformedAttachPos); } } } @@ -268,9 +271,12 @@ namespace Barotrauma } } - private void AttachToBody(PhysicsBody collider, Limb attachLimb, Body targetBody, Vector2 attachPos) + private void AttachToBody(Vector2 attachPos) { + if (attachLimb == null) { return; } + if (targetBody == null) { return; } if (attachCooldown > 0) { return; } + var collider = character.AnimController.Collider; //already attached to something if (AttachJoints.Count > 0) { 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 b63a77d34..23a43a38a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs @@ -67,6 +67,12 @@ namespace Barotrauma _abandon = value; if (_abandon) { +#if DEBUG + if (HumanAIController.debugai && objectiveManager.IsOrder(this) && !objectiveManager.IsCurrentOrder()) + { + throw new Exception("Order abandoned!"); + } +#endif OnAbandon(); } } @@ -96,9 +102,21 @@ namespace Barotrauma return all; } + /// + /// A single shot event. Automatically cleared after launching. Use OnCompleted method for implementing (internal) persistent behavior. + /// public event Action Completed; + /// + /// A single shot event. Automatically cleared after launching. Use OnAbandoned method for implementing (internal) persistent behavior. + /// public event Action Abandoned; + /// + /// A single shot event. Automatically cleared after launching. Use OnSelected method for implementing (internal) persistent behavior. + /// public event Action Selected; + /// + /// A single shot event. Automatically cleared after launching. Use OnDeselected method for implementing (internal) persistent behavior. + /// public event Action Deselected; protected HumanAIController HumanAIController => character.AIController as HumanAIController; @@ -202,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); } } @@ -212,7 +230,7 @@ namespace Barotrauma /// public virtual float GetPriority() { - bool isOrder = objectiveManager.CurrentOrder == this; + bool isOrder = objectiveManager.IsOrder(this); if (!IsAllowed) { Priority = 0; @@ -221,7 +239,7 @@ namespace Barotrauma } if (isOrder) { - Priority = AIObjectiveManager.OrderPriority; + Priority = objectiveManager.GetOrderPriority(this); } else { @@ -243,7 +261,7 @@ namespace Barotrauma public virtual void Update(float deltaTime) { - if (objectiveManager.CurrentOrder != this && objectiveManager.WaitTimer <= 0) + if (!objectiveManager.IsOrder(this) && objectiveManager.WaitTimer <= 0) { UpdateDevotion(deltaTime); } @@ -318,22 +336,26 @@ namespace Barotrauma { Reset(); Selected?.Invoke(); + Selected = null; } public virtual void OnDeselected() { CumulatedDevotion = 0; Deselected?.Invoke(); + Deselected = null; } protected virtual void OnCompleted() { Completed?.Invoke(); + Completed = null; } protected virtual void OnAbandon() { Abandoned?.Invoke(); + Abandoned = null; } public virtual void Reset() @@ -408,7 +430,14 @@ namespace Barotrauma subObjectives.Remove(subObjective); if (AbandonWhenCannotCompleteSubjectives) { - Abandon = true; + if (objectiveManager.IsOrder(this)) + { + Reset(); + } + else + { + Abandon = true; + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs index efea317ca..1835b0a4f 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; } @@ -64,7 +64,7 @@ namespace Barotrauma private bool IsReady(PowerContainer battery) { - if (battery.HasBeenTuned && character.CurrentOrder == null) { return true; } + if (battery.HasBeenTuned && character.IsDismissed) { return true; } if (Option == "charge") { return battery.RechargeRatio >= PowerContainer.aiRechargeTargetRatio; @@ -79,7 +79,7 @@ namespace Barotrauma new AIObjectiveOperateItem(battery, character, objectiveManager, Option, false, priorityModifier: PriorityModifier) { IsLoop = false, - Override = character.CurrentOrder != null, + Override = !character.IsDismissed, completionCondition = () => IsReady(battery) }; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs index feb1032b7..919eb6f0a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs @@ -48,7 +48,7 @@ namespace Barotrauma float selectedBonus = isSelected ? 100 - MaxDevotion : 0; float devotion = (CumulatedDevotion + selectedBonus) / 100; float reduction = IsPriority ? 1 : isSelected ? 2 : 3; - float max = MathHelper.Min(AIObjectiveManager.OrderPriority - reduction, 90); + float max = AIObjectiveManager.LowestOrderPriority - reduction; Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (distanceFactor * PriorityModifier), 0, 1)); } return Priority; @@ -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; @@ -74,6 +79,7 @@ namespace Barotrauma TryAddSubObjective(ref decontainObjective, () => new AIObjectiveDecontainItem(character, item, objectiveManager, targetContainer: suitableContainer.GetComponent()) { Equip = equip, + TakeWholeStack = true, DropIfFails = true }, onCompleted: () => diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs index c6d12d979..73bb788a6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs @@ -12,21 +12,30 @@ 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.IsOrder(this) ? objectiveManager.GetOrderPriority(this) : AIObjectiveManager.RunPriority - 1) : 0; protected override bool Filter(Item target) { // If the target was selected as a valid target, we'll have to accept it so that the objective can be completed. // The validity changes when a character picks the item up. - if (!IsValidTarget(target, character)) { return Objectives.ContainsKey(target) && IsItemInsideValidSubmarine(target, character); } + if (!IsValidTarget(target, character, checkInventory: true)) { return Objectives.ContainsKey(target) && IsItemInsideValidSubmarine(target, character); } if (target.CurrentHull.FireSources.Count > 0) { return false; } // Don't repair items in rooms that have enemies inside. if (Character.CharacterList.Any(c => c.CurrentHull == target.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; } @@ -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 IsValidTarget(Item item, Character character) + 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; } @@ -83,20 +99,29 @@ namespace Barotrauma { return false; } + if (!checkInventory) + { + return true; + } bool canEquip = true; if (!item.AllowedSlots.Contains(InvSlotType.Any)) { canEquip = false; + var inv = character.Inventory; foreach (var allowedSlot in item.AllowedSlots) { - int slot = character.Inventory.FindLimbSlot(allowedSlot); - if (slot > -1) + foreach (var slotType in inv.SlotTypes) { - if (character.Inventory.Items[slot] == null) + if (!allowedSlot.HasFlag(slotType)) { continue; } + for (int i = 0; i < inv.Capacity; i++) { canEquip = true; - break; - } + if (allowedSlot.HasFlag(inv.SlotTypes[i]) && inv.GetItemAt(i) != null) + { + canEquip = false; + break; + } + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index 7311434af..a1a9b6999 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -79,11 +79,17 @@ namespace Barotrauma private float coolDownTimer; private IEnumerable myBodies; private float aimTimer; + private float spreadTimer; private bool canSeeTarget; private float visibilityCheckTimer; private readonly float visibilityCheckInterval = 0.2f; + private float sqrDistance; + private readonly float maxDistance = 2000; + private readonly float distanceCheckInterval = 0.2f; + private float distanceTimer; + /// /// Aborts the objective when this condition is true /// @@ -108,9 +114,13 @@ namespace Barotrauma public CombatMode Mode { get; private set; } private bool IsOffensiveOrArrest => initialMode == CombatMode.Offensive || initialMode == CombatMode.Arrest; - private bool TargetEliminated => Enemy == null || Enemy.Removed || Enemy.IsUnconscious; + private bool TargetEliminated => IsEnemyDisabled || Enemy.IsUnconscious; private bool IsEnemyDisabled => Enemy == null || Enemy.Removed || Enemy.IsDead; - private bool EnemyIsClose() => Enemy != null && character.CurrentHull == Enemy.CurrentHull || Vector2.DistanceSquared(character.Position, Enemy.Position) < 500; + + private float AimSpeed => HumanAIController.AimSpeed; + private float AimAccuracy => HumanAIController.AimAccuracy; + + private bool EnemyIsClose() => Enemy != null && character.CurrentHull != null && character.CurrentHull == Enemy.CurrentHull || Vector2.DistanceSquared(character.Position, Enemy.Position) < 500; public AIObjectiveCombat(Character character, Character enemy, CombatMode mode, AIObjectiveManager objectiveManager, float priorityModifier = 1, float coolDown = 10.0f) : base(character, objectiveManager, priorityModifier) @@ -136,11 +146,12 @@ namespace Barotrauma { Mode = CombatMode.Retreat; } + spreadTimer = Rand.Range(-10, 10); } 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)) { @@ -168,6 +179,12 @@ namespace Barotrauma { findSafety.Priority = 0; } + distanceTimer -= deltaTime; + if (distanceTimer < 0) + { + distanceTimer = distanceCheckInterval; + sqrDistance = Vector2.DistanceSquared(character.WorldPosition, Enemy.WorldPosition); + } } protected override bool Check() @@ -175,9 +192,13 @@ namespace Barotrauma if (IsOffensiveOrArrest && Mode != initialMode) { Abandon = true; - SteeringManager.Reset(); return false; } + if (sqrDistance > maxDistance * maxDistance) + { + // The target escaped from us. + return true; + } return IsEnemyDisabled || (!IsOffensiveOrArrest && coolDownTimer <= 0); } @@ -186,7 +207,6 @@ namespace Barotrauma if (abortCondition != null && abortCondition()) { Abandon = true; - SteeringManager.Reset(); return; } if (!IsOffensiveOrArrest) @@ -238,7 +258,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 +282,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 +326,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 +391,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 +586,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 +619,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,10 +638,10 @@ 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); + aimTimer = Rand.Range(1f, 1.5f) / AimSpeed; } else { @@ -651,7 +672,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); } } @@ -705,11 +726,7 @@ namespace Barotrauma DialogueIdentifier = "dialogcannotreachtarget", TargetName = Enemy.DisplayName }, - onAbandon: () => - { - Abandon = true; - SteeringManager.Reset(); - }); + onAbandon: () => Abandon = true); if (followTargetObjective == null) { return; } if (Mode == CombatMode.Arrest && Enemy.Stun > 2) { @@ -724,7 +741,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 +786,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 +831,32 @@ 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 (containedItem == null) { continue; } - if (containedItem.Condition <= 0) - { - containedItem.Drop(character); - } - } + if (WeaponComponent == null) { return false; } + if (Weapon.OwnInventory == null) { return true; } + // Eject empty ammo + HumanAIController.UnequipEmptyItems(Weapon); 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) { @@ -851,22 +867,13 @@ namespace Barotrauma if (ammunition != null) { var container = Weapon.GetComponent(); - if (container.Item.ParentInventory == character.Inventory) + if (!container.Inventory.TryPutItem(ammunition, null)) { - if (!container.Inventory.CanBePut(ammunition)) - { - return false; - } - character.Inventory.RemoveItem(ammunition); - if (!container.Inventory.TryPutItem(ammunition, null)) + if (ammunition.ParentInventory == character.Inventory) { ammunition.Drop(character); } } - else - { - container.Combine(ammunition, character); - } } } } @@ -884,6 +891,15 @@ namespace Barotrauma private void Attack(float deltaTime) { character.CursorPosition = Enemy.WorldPosition; + if (AimAccuracy < 1) + { + spreadTimer += deltaTime * Rand.Range(0.01f, 1f); + float shake = Rand.Range(0.95f, 1.05f); + float offsetAmount = (1 - AimAccuracy) * Rand.Range(300f, 500f); + float distanceFactor = MathUtils.InverseLerp(0, 1000 * 1000, sqrDistance); + float offset = (float)Math.Sin(spreadTimer * shake) * offsetAmount * distanceFactor; + character.CursorPosition += new Vector2(0, offset); + } if (character.Submarine != null) { character.CursorPosition -= character.Submarine.Position; @@ -894,7 +910,11 @@ namespace Barotrauma canSeeTarget = character.CanSeeTarget(Enemy); visibilityCheckTimer = visibilityCheckInterval; } - if (!canSeeTarget) { return; } + if (!canSeeTarget) + { + aimTimer = Rand.Range(0.2f, 1f) / AimSpeed; + return; + } if (Weapon.RequireAimToUse) { character.SetInput(InputType.Aim, false, true); @@ -945,14 +965,12 @@ namespace Barotrauma } if (closeEnough) { - SteeringManager.Reset(); - character.SetInput(InputType.Shoot, false, true); - Weapon.Use(deltaTime, character); + UseWeapon(deltaTime); } else if (!character.IsFacing(Enemy.WorldPosition)) { // Don't do the facing check if we are close to the target, because it easily causes the character to get stuck here when it flips around. - aimTimer = Rand.Range(1f, 1.5f); + aimTimer = Rand.Range(1f, 1.5f) / AimSpeed; } } else @@ -961,14 +979,15 @@ namespace Barotrauma { if (sqrDist > repairTool.Range * repairTool.Range) { return; } } - if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.Position - Weapon.Position) < MathHelper.PiOver4) + float aimFactor = MathHelper.PiOver2 * (1 - AimAccuracy); + if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.Position - Weapon.Position) < MathHelper.PiOver4 + aimFactor) { if (myBodies == null) { myBodies = character.AnimController.Limbs.Select(l => l.body.FarseerBody); } var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel; - var pickedBody = Submarine.PickBody(Weapon.SimPosition, Enemy.SimPosition, myBodies, collisionCategories); + var pickedBody = Submarine.PickBody(Weapon.SimPosition, Enemy.SimPosition, myBodies, collisionCategories, allowInsideFixture: true); if (pickedBody != null) { Character target = null; @@ -982,24 +1001,29 @@ namespace Barotrauma } if (target != null && (target == Enemy || !HumanAIController.IsFriendly(target))) { - character.SetInput(InputType.Shoot, false, true); - Weapon.Use(deltaTime, character); - float reloadTime = 0; - if (WeaponComponent is RangedWeapon rangedWeapon) - { - reloadTime = rangedWeapon.Reload; - } - if (WeaponComponent is MeleeWeapon mw) - { - reloadTime = mw.Reload; - } - aimTimer = reloadTime * Rand.Range(1f, 1.5f); + UseWeapon(deltaTime); } } } } } + private void UseWeapon(float deltaTime) + { + character.SetInput(InputType.Shoot, false, true); + Weapon.Use(deltaTime, character); + float reloadTime = 0; + if (WeaponComponent is RangedWeapon rangedWeapon) + { + reloadTime = rangedWeapon.Reload; + } + if (WeaponComponent is MeleeWeapon mw) + { + reloadTime = mw.Reload; + } + aimTimer = Math.Max(reloadTime, reloadTime * Rand.Range(1f, 1.5f) / AimSpeed); + } + protected override void OnCompleted() { base.OnCompleted(); @@ -1007,6 +1031,23 @@ namespace Barotrauma { Unequip(); } + if (!HoldPosition) + { + SteeringManager.Reset(); + } + } + + protected override void OnAbandon() + { + base.OnAbandon(); + if (Weapon != null) + { + Unequip(); + } + if (!HoldPosition) + { + SteeringManager.Reset(); + } } public override void Reset() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs index e71b389f8..165e53e47 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs @@ -21,7 +21,8 @@ namespace Barotrauma //can either be a tag or an identifier public readonly string[] itemIdentifiers; public readonly ItemContainer container; - public readonly Item item; + private readonly Item item; + public Item ItemToContain { get; private set; } private AIObjectiveGetItem getItemObjective; private AIObjectiveGoTo goToObjective; @@ -30,10 +31,13 @@ namespace Barotrauma public bool AllowToFindDivingGear { get; set; } = true; public bool AllowDangerousPressure { get; set; } - public float ConditionLevel { get; set; } + public float ConditionLevel { get; set; } = 1; public bool Equip { get; set; } public bool RemoveEmpty { get; set; } = true; + public bool MoveWholeStack { get; set; } + + public AIObjectiveContainItem(Character character, Item item, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { @@ -66,14 +70,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++; } @@ -82,7 +86,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) { @@ -91,58 +95,44 @@ namespace Barotrauma Abandon = true; return; } - Item itemToContain = item ?? character.Inventory.FindItem(i => CheckItem(i) && i.Container != container.Item, recursive: true); - if (itemToContain != null) + ItemToContain = item ?? character.Inventory.FindItem(i => CheckItem(i) && i.Container != container.Item, recursive: true); + if (ItemToContain != null) { - if (!character.CanInteractWith(itemToContain)) + if (!character.CanInteractWith(ItemToContain, checkLinked: false)) { Abandon = true; return; } - if (character.CanInteractWith(container.Item, out _, checkLinked: false)) + if (character.CanInteractWith(container.Item, checkLinked: false)) { if (RemoveEmpty) { - foreach (var emptyItem in container.Inventory.Items) - { - if (emptyItem == null) { continue; } - if (emptyItem.Condition <= 0) - { - emptyItem.Drop(character); - } - } + HumanAIController.UnequipEmptyItems(container.Item); } - // Contain the item - if (itemToContain.ParentInventory == character.Inventory) + Inventory originalInventory = ItemToContain.ParentInventory; + var slots = originalInventory?.FindIndices(ItemToContain); + if (container.Inventory.TryPutItem(ItemToContain, null)) { - if (!container.Inventory.CanBePut(itemToContain)) + if (MoveWholeStack && slots != null) { - Abandon = true; - } - else - { - character.Inventory.RemoveItem(itemToContain); - if (container.Inventory.TryPutItem(itemToContain, null)) + foreach (int slot in slots) { - IsCompleted = true; - } - else - { - itemToContain.Drop(character); - Abandon = true; + foreach (Item item in originalInventory.GetItemsAt(slot).ToList()) + { + container.Inventory.TryPutItem(item, null); + } } + + IsCompleted = true; } } else { - if (container.Combine(itemToContain, character)) + if (ItemToContain.ParentInventory == character.Inventory) { - IsCompleted = true; - } - else - { - Abandon = true; + ItemToContain.Drop(character); } + Abandon = true; } } else @@ -151,7 +141,7 @@ namespace Barotrauma { DialogueIdentifier = "dialogcannotreachtarget", TargetName = container.Item.Name, - abortCondition = () => !itemToContain.IsOwnedBy(character) + abortCondition = obj => !ItemToContain.IsOwnedBy(character) }, onAbandon: () => Abandon = true, onCompleted: () => RemoveSubObjective(ref goToObjective)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs index b00e588cd..63db2719a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs @@ -22,8 +22,13 @@ namespace Barotrauma public AIObjectiveGetItem GetItemObjective => getItemObjective; public AIObjectiveContainItem ContainObjective => containObjective; + public Item TargetItem => targetItem; + public ItemContainer TargetContainer => targetContainer; + public bool Equip { get; set; } + public bool TakeWholeStack { get; set; } + /// /// If true drops the item when containing the item fails. /// In both cases abandons the objective. @@ -58,12 +63,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 +87,7 @@ namespace Barotrauma return; } } - else if (targetContainer.Inventory.Items.Contains(itemToDecontain)) + else if (targetContainer.Inventory.Contains(itemToDecontain)) { IsCompleted = true; return; @@ -85,7 +95,7 @@ namespace Barotrauma if (getItemObjective == null && !itemToDecontain.IsOwnedBy(character)) { TryAddSubObjective(ref getItemObjective, - constructor: () => new AIObjectiveGetItem(character, targetItem, objectiveManager, Equip), + constructor: () => new AIObjectiveGetItem(character, targetItem, objectiveManager, Equip) { TakeWholeStack = this.TakeWholeStack }, onAbandon: () => Abandon = true); return; } @@ -94,6 +104,7 @@ namespace Barotrauma TryAddSubObjective(ref containObjective, constructor: () => new AIObjectiveContainItem(character, itemToDecontain, targetContainer, objectiveManager) { + MoveWholeStack = TakeWholeStack, Equip = Equip, RemoveEmpty = false, GetItemPriority = GetItemPriority, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs index defc6fc18..25f2082ee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs @@ -35,7 +35,7 @@ namespace Barotrauma Abandon = true; return Priority; } - bool isOrder = objectiveManager.IsCurrentOrder(); + bool isOrder = objectiveManager.HasOrder(); if (!isOrder && Character.CharacterList.Any(c => c.CurrentHull == targetHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { // Don't go into rooms with any enemies, unless it's an order @@ -78,14 +78,17 @@ namespace Barotrauma { TryAddSubObjective(ref getExtinguisherObjective, () => { - character.Speak(TextManager.Get("DialogFindExtinguisher"), null, 2.0f, "findextinguisher", 30.0f); + if (character.IsOnPlayerTeam && !character.HasEquippedItem("fireextinguisher", allowBroken: false)) + { + character.Speak(TextManager.Get("DialogFindExtinguisher"), null, 2.0f, "findextinguisher", 30.0f); + } var getItemObjective = new AIObjectiveGetItem(character, "fireextinguisher", objectiveManager, equip: true) { AllowStealing = true, // If the item is inside an unsafe hull, decrease the priority GetItemPriority = i => HumanAIController.UnsafeHulls.Contains(i.CurrentHull) ? 0.1f : 1 }; - if (objectiveManager.IsCurrentOrder()) + if (objectiveManager.HasOrder()) { getItemObjective.Abandoned += () => character.Speak(TextManager.Get("dialogcannotfindfireextinguisher"), null, 0.0f, "dialogcannotfindfireextinguisher", 10.0f); }; @@ -105,9 +108,13 @@ namespace Barotrauma } foreach (FireSource fs in targetHull.FireSources) { - bool inRange = fs.IsInDamageRange(character, MathHelper.Clamp(fs.DamageRange * 1.5f, extinguisher.Range * 0.5f, extinguisher.Range)); - bool move = !inRange || !HumanAIController.VisibleHulls.Contains(fs.Hull); - if (inRange || useExtinquisherTimer > 0.0f) + 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; + // 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) @@ -121,19 +128,7 @@ namespace Barotrauma character.CursorPosition += VectorExtensions.Forward(extinguisherItem.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, dist / 2); if (extinguisherItem.RequireAimToUse) { - bool isOperatingButtons = false; - if (SteeringManager == PathSteering) - { - var door = PathSteering.CurrentPath?.CurrentNode?.ConnectedDoor; - if (door != null && !door.IsOpen && !door.IsBroken) - { - isOperatingButtons = door.HasIntegratedButtons || door.Item.GetConnectedComponents(true).Any(); - } - } - if (!isOperatingButtons) - { - character.SetInput(InputType.Aim, false, true); - } + character.SetInput(InputType.Aim, false, true); sinTime += deltaTime * 10; } character.SetInput(extinguisherItem.IsShootable ? InputType.Shoot : InputType.Use, false, true); @@ -142,15 +137,11 @@ namespace Barotrauma { character.Speak(TextManager.GetWithVariable("DialogPutOutFire", "[roomname]", targetHull.DisplayName, true), null, 0, "putoutfire", 10.0f); } - if (!character.CanSeeTarget(fs)) - { - move = true; - } } if (move) { //go to the first firesource - if (TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager, closeEnough: extinguisher.Range / 2) + if (TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager, closeEnough: Math.Max(fs.DamageRange, extinguisher.Range * 0.7f)) { DialogueIdentifier = "dialogcannotreachfire", TargetName = fs.Hull.DisplayName @@ -158,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..2422534e2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs @@ -38,11 +38,19 @@ 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; } if (!character.Submarine.IsEntityFoundOnThisSub(hull, includingConnectedSubs: true)) { return false; } + if (hull.BallastFlora != null) { return false; } + foreach (var ballastFlora in MapCreatures.Behavior.BallastFloraBehavior.EntityList) + { + if (ballastFlora.Parent?.Submarine != character.Submarine) { continue; } + if (ballastFlora.Branches.Any(b => !b.Removed && b.Health > 0 && b.CurrentHull == hull)) + { + return false; + } + } return true; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs index 11cd94e7f..1c4e86800 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) @@ -50,14 +50,11 @@ namespace Barotrauma { if (target == null || target.IsDead || target.Removed) { return false; } if (target == character) { return false; } - if (HumanAIController.IsFriendly(character, target)) { return false; } if (target.Submarine == null) { return false; } - if (target.Submarine.TeamID != character.TeamID) { return false; } + if (character.Submarine == null) { return false; } if (target.CurrentHull == null) { return false; } - if (character.Submarine != null) - { - if (!character.Submarine.IsConnectedTo(target.Submarine)) { return false; } - } + if (HumanAIController.IsFriendly(character, target)) { return false; } + if (!character.Submarine.IsConnectedTo(target.Submarine)) { return false; } return true; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs index 9ffd134f4..4c11ccbcc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs @@ -1,5 +1,7 @@ using Barotrauma.Items.Components; using Barotrauma.Extensions; +using System.Collections.Generic; +using System.Linq; namespace Barotrauma { @@ -36,11 +38,11 @@ namespace Barotrauma return; } targetItem = character.Inventory.FindItemByTag(gearTag, true); - if (targetItem == null || !character.HasEquippedItem(targetItem)) + if (targetItem == null || !character.HasEquippedItem(targetItem) && targetItem.ContainedItems.Any(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > 0)) { TryAddSubObjective(ref getDivingGear, () => { - if (targetItem == null) + if (targetItem == null && character.IsOnPlayerTeam) { character.Speak(TextManager.Get("DialogGetDivingGear"), null, 0.0f, "getdivinggear", 30.0f); } @@ -56,7 +58,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"); @@ -64,14 +66,25 @@ namespace Barotrauma Abandon = true; return; } - if (containedItems.None(it => it != null && it.HasTag(OXYGEN_SOURCE) && it.Condition > MIN_OXYGEN)) + float min = character.Submarine == null ? 0.01f : MIN_OXYGEN; + if (containedItems.None(it => it != null && it.HasTag(OXYGEN_SOURCE) && it.Condition > min)) { // No valid oxygen source loaded. - // Seek oxygen that has min 10% condition left. + // Seek oxygen that has at least 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 (character.IsOnPlayerTeam) + { + if (HumanAIController.HasItem(character, "oxygensource", out _, conditionPercentage: min)) + { + character.Speak(TextManager.Get("dialogswappingoxygentank"), null, 0, "swappingoxygentank", 30.0f); + } + else + { + 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, @@ -80,20 +93,45 @@ namespace Barotrauma }, onAbandon: () => { + int remainingTanks = ReportOxygenTankCount(); // 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, - ConditionLevel = 0 + AllowDangerousPressure = true }; }, - onAbandon: () => Abandon = true, + onAbandon: () => + { + Abandon = true; + if (remainingTanks > 0 && !HumanAIController.HasItem(character, "oxygensource", out _, conditionPercentage: 0.01f)) + { + character.Speak(TextManager.Get("dialogcantfindtoxygen"), null, 0, "cantfindoxygen", 30.0f); + } + }, onCompleted: () => RemoveSubObjective(ref getOxygen)); }, - onCompleted: () => RemoveSubObjective(ref getOxygen)); + onCompleted: () => + { + RemoveSubObjective(ref getOxygen); + ReportOxygenTankCount(); + }); + + int ReportOxygenTankCount() + { + int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag("oxygensource") && i.Condition > 1); + if (remainingOxygenTanks == 0) + { + character.Speak(TextManager.Get("DialogOutOfOxygenTanks"), null, 0.0f, "outofoxygentanks", 30.0f); + } + else if (remainingOxygenTanks < 10) + { + character.Speak(TextManager.Get("DialogLowOnOxygenTanks"), null, 0.0f, "lowonoxygentanks", 30.0f); + } + return remainingOxygenTanks; + } } } } @@ -101,21 +139,11 @@ 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) - { - return false; - } - foreach (Item containedItem in containedItems) - { - if (containedItem == null) { continue; } - if (containedItem.Condition <= 0.0f) - { - containedItem.Drop(actor); - } - } + containedItems = target.OwnInventory?.AllItems; + if (containedItems == null) { return false; } + AIController.UnequipEmptyItems(actor, target); return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs index 1d98fcf52..3fa283a4d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs @@ -46,19 +46,24 @@ namespace Barotrauma } if (character.CurrentHull == null) { - Priority = objectiveManager.CurrentOrder is AIObjectiveGoTo && HumanAIController.HasDivingSuit(character) ? 0 : 100; + Priority = (objectiveManager.IsCurrentOrder() || objectiveManager.HasActiveObjective()) && HumanAIController.HasDivingSuit(character) ? 0 : 100; } else { - if (HumanAIController.NeedsDivingGear(character.CurrentHull, out _) && !HumanAIController.HasDivingGear(character)) + if (HumanAIController.NeedsDivingGear(character.CurrentHull, out _) && !HumanAIController.HasDivingGear(character, conditionPercentage: AIObjectiveFindDivingGear.MIN_OXYGEN)) { Priority = 100; } + else if (objectiveManager.IsCurrentOrder() && character.Submarine != null && !HumanAIController.IsOnFriendlyTeam(character.TeamID, character.Submarine.TeamID)) + { + // Ordered to follow/hold position inside a hostile sub -> ignore find safety unless we need to find a diving gear + Priority = 0; + } Priority = MathHelper.Clamp(Priority, 0, 100); if (divingGearObjective != null && !divingGearObjective.IsCompleted && divingGearObjective.CanBeCompleted) { // Boost the priority while seeking the diving gear - Priority = Math.Max(Priority, Math.Min(AIObjectiveManager.OrderPriority + 20, 100)); + Priority = Math.Max(Priority, Math.Min(AIObjectiveManager.HighestOrderPriority + 20, 100)); } } return Priority; @@ -168,7 +173,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 +364,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..86257f0b6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs @@ -52,7 +52,7 @@ namespace Barotrauma float distanceFactor = isPriority || xDist < 200 && yDist < 100 ? 1 : MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 3000, xDist + yDist * 3.0f)); float severity = isPriority ? 1 : AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100; float reduction = isPriority ? 1 : 2; - float max = MathHelper.Min(AIObjectiveManager.OrderPriority - reduction, 90); + float max = AIObjectiveManager.LowestOrderPriority - reduction; float devotion = CumulatedDevotion / 100; Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1)); } @@ -64,10 +64,10 @@ 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()) + if (character.IsOnPlayerTeam && objectiveManager.IsCurrentOrder()) { character.Speak(TextManager.Get("dialogcannotfindweldingequipment"), null, 0.0f, "dialogcannotfindweldingequipment", 10.0f); } @@ -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,19 +87,34 @@ namespace Barotrauma return; } // Drop empty tanks - foreach (Item containedItem in containedItems) + HumanAIController.UnequipEmptyItems(weldingTool); + + if (weldingTool.OwnInventory.AllItems.None(i => i.HasTag("weldingfuel") && i.Condition > 0.0f)) { - if (containedItem == null) { continue; } - if (containedItem.Condition <= 0.0f) + TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel", weldingTool.GetComponent(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC), + onAbandon: () => + { + Abandon = true; + ReportWeldingFuelTankCount(); + }, + onCompleted: () => + { + RemoveSubObjective(ref refuelObjective); + ReportWeldingFuelTankCount(); + }); + + void ReportWeldingFuelTankCount() { - containedItem.Drop(character); + int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag("weldingfuel") && i.Condition > 1); + if (remainingOxygenTanks == 0) + { + character.Speak(TextManager.Get("DialogOutOfWeldingFuel"), null, 0.0f, "outofweldingfuel", 30.0f); + } + else if (remainingOxygenTanks < 4) + { + character.Speak(TextManager.Get("DialogLowOnWeldingFuel"), null, 0.0f, "lowonweldingfuel", 30.0f); + } } - } - if (containedItems.None(i => i != null && i.HasTag("weldingfuel") && i.Condition > 0.0f)) - { - TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel", weldingTool.GetComponent(), objectiveManager, spawnItemIfNotFound: character.TeamID == Character.TeamType.FriendlyNPC), - onAbandon: () => Abandon = true, - onCompleted: () => RemoveSubObjective(ref refuelObjective)); return; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs index cddc1dd30..4e3e1f6a1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs @@ -42,7 +42,7 @@ namespace Barotrauma if (totalLeaks == 0) { return 0; } int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective() && !c.Character.IsIncapacitated, onlyBots: true); bool anyFixers = otherFixers > 0; - if (objectiveManager.CurrentOrder == this) + if (objectiveManager.IsOrder(this)) { float ratio = anyFixers ? totalLeaks / (float)otherFixers : 1; return Targets.Sum(t => GetLeakSeverity(t)) * ratio; @@ -72,7 +72,11 @@ namespace Barotrauma { if (gap == null) { return false; } // Don't fix a leak on a wall section set to be ignored - if (gap.ConnectedWall?.Sections?.Any(s => s.gap == gap && s.IgnoreByAI) ?? false) { return false; } + if (gap.ConnectedWall != null) + { + if (gap.ConnectedWall.Sections.Any(s => s.gap == gap && s.IgnoreByAI)) { return false; } + if (gap.ConnectedWall.MaxHealth <= 0.0f) { return false; } + } if (gap.ConnectedWall == null || gap.ConnectedDoor != null || gap.Open <= 0 || gap.linkedTo.All(l => l == null)) { return false; } if (gap.Submarine == null || character.Submarine == null) { return false; } // Don't allow going into another sub, unless it's connected and of the same team and type. diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs index ccb23c9e3..224bb6ed5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs @@ -44,6 +44,8 @@ namespace Barotrauma /// public bool AllowStealing { get; set; } + public bool TakeWholeStack { get; set; } + public AIObjectiveGetItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, bool equip = true, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { @@ -177,7 +179,7 @@ namespace Barotrauma } else if (moveToTarget is Item parentItem) { - canInteract = character.CanInteractWith(parentItem, out _, checkLinked: false); + canInteract = character.CanInteractWith(parentItem, checkLinked: false); } if (canInteract) { @@ -191,8 +193,20 @@ namespace Barotrauma return; } + Inventory itemInventory = targetItem.ParentInventory; + var slots = itemInventory?.FindIndices(targetItem); if (HumanAIController.TakeItem(targetItem, character.Inventory, equip, storeUnequipped: true)) { + if (TakeWholeStack && slots != null) + { + foreach (int slot in slots) + { + foreach (Item item in itemInventory.GetItemsAt(slot).ToList()) + { + HumanAIController.TakeItem(item, character.Inventory, equip: false, storeUnequipped: true); + } + } + } IsCompleted = true; } else @@ -211,7 +225,16 @@ namespace Barotrauma return new AIObjectiveGoTo(moveToTarget, character, objectiveManager, repeat: false, getDivingGearIfNeeded: AllowToFindDivingGear, closeEnough: DefaultReach) { // If the root container changes, the item is no longer where it was (taken by someone -> need to find another item) - abortCondition = () => targetItem == null || targetItem.GetRootInventoryOwner() != moveToTarget, + abortCondition = obj => + { + bool abort = targetItem == null || targetItem.GetRootInventoryOwner() != moveToTarget; + if (abort) + { + // Fail silently if someone takes the suit. + obj.speakIfFails = false; + } + return abort; + }, DialogueIdentifier = "dialogcannotreachtarget", TargetName = (moveToTarget as MapEntity)?.Name ?? (moveToTarget as Character)?.Name ?? moveToTarget.ToString() }; @@ -256,7 +279,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 +299,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 +335,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 +374,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..5b7be664b 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 { @@ -22,18 +23,22 @@ namespace Barotrauma /// /// Aborts the objective when this condition is true /// - public Func abortCondition; + public Func abortCondition; public Func endNodeFilter; public Func priorityGetter; public bool followControlledCharacter; public bool mimic; + public bool speakIfFails = true; public float extraDistanceWhileSwimming; public float extraDistanceOutsideSub; private float _closeEnough = 50; private readonly float minDistance = 50; + private readonly float seekGapsInterval = 1; + private float seekGapsTimer; + /// /// Display units /// @@ -76,7 +81,7 @@ namespace Barotrauma public override float GetPriority() { - bool isOrder = objectiveManager.CurrentOrder == this; + bool isOrder = objectiveManager.IsOrder(this); if (!IsAllowed) { Priority = 0; @@ -110,12 +115,14 @@ namespace Barotrauma } else { - Priority = isOrder ? AIObjectiveManager.OrderPriority : 10; + Priority = isOrder ? objectiveManager.GetOrderPriority(this) : 10; } } 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) { @@ -140,10 +147,11 @@ namespace Barotrauma private void SpeakCannotReach() { + if (!character.IsOnPlayerTeam) { return; } #if DEBUG DebugConsole.NewMessage($"{character.Name}: Cannot reach the target: {Target}", Color.Yellow); #endif - if (objectiveManager.CurrentOrder != null && DialogueIdentifier != null) + if (objectiveManager.HasOrders() && DialogueIdentifier != null && speakIfFails) { string msg = TargetName == null ? TextManager.Get(DialogueIdentifier, true) : TextManager.GetWithVariable(DialogueIdentifier, "[name]", TargetName, formatCapitals: !(Target is Character)); if (msg != null) @@ -157,10 +165,9 @@ namespace Barotrauma { if (followControlledCharacter) { - if (Character.Controlled == null) + if (Character.Controlled == null || !HumanAIController.IsFriendly(Character.Controlled)) { Abandon = true; - SteeringManager.Reset(); return; } Target = Character.Controlled; @@ -181,7 +188,6 @@ namespace Barotrauma if (e.Removed) { Abandon = true; - SteeringManager.Reset(); return; } else @@ -193,7 +199,7 @@ namespace Barotrauma if (!followControlledCharacter) { // Abandon if going through unsafe paths. Note ignores unsafe nodes when following an order or when the objective is set to ignore unsafe hulls. - bool containsUnsafeNodes = HumanAIController.CurrentOrder == null && !HumanAIController.ObjectiveManager.CurrentObjective.IgnoreUnsafeHulls + bool containsUnsafeNodes = character.IsDismissed && !HumanAIController.ObjectiveManager.CurrentObjective.IgnoreUnsafeHulls && PathSteering != null && PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull)); if (containsUnsafeNodes || HumanAIController.UnreachableHulls.Contains(targetHull)) @@ -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) { @@ -248,13 +249,14 @@ namespace Barotrauma } } bool needsEquipment = false; + float minOxygen = character.Submarine == null ? 0 : AIObjectiveFindDivingGear.MIN_OXYGEN; if (needsDivingSuit) { - needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.MIN_OXYGEN); + needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen); } else if (needsDivingGear) { - needsEquipment = !HumanAIController.HasDivingGear(character, AIObjectiveFindDivingGear.MIN_OXYGEN); + needsEquipment = !HumanAIController.HasDivingGear(character, minOxygen); } if (needsEquipment) { @@ -286,52 +288,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 +431,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 +442,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 +462,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 +523,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 @@ -465,7 +570,7 @@ namespace Barotrauma Abandon = true; return false; } - if (abortCondition != null && abortCondition()) + if (abortCondition != null && abortCondition(this)) { Abandon = true; return false; @@ -507,12 +612,13 @@ namespace Barotrauma { PathSteering.ResetPath(); } + SpeakCannotReach(); base.OnAbandon(); } private void StopMovement() { - character.AIController.SteeringManager.Reset(); + SteeringManager.Reset(); if (Target != null) { character.AnimController.TargetDir = Target.WorldPosition.X > character.WorldPosition.X ? Direction.Right : Direction.Left; @@ -530,6 +636,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 da1b8e492..665639722 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs @@ -21,9 +21,9 @@ namespace Barotrauma set { behavior = value; - if (behavior == BehaviorType.StayInHull && character.TeamID != Character.TeamType.FriendlyNPC) + if (behavior == BehaviorType.StayInHull && TargetHull == null) { - 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); + DebugConsole.AddWarning($"Trying to set a character's behavior type to StayInHull, but target hull is not set. {character.Name} ({character.Info.Job.Prefab.Identifier})"); behavior = BehaviorType.Passive; } switch (behavior) @@ -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 => { @@ -319,14 +319,14 @@ namespace Barotrauma public void Wander(float deltaTime) { if (character.IsClimbing) { return; } - if (!character.AnimController.InWater) + var currentHull = character.CurrentHull; + if (!character.AnimController.InWater && currentHull != null) { standStillTimer -= deltaTime; if (standStillTimer > 0.0f) { walkDuration = Rand.Range(walkDurationMin, walkDurationMax); - var currentHull = character.CurrentHull; - if (currentHull != null && currentHull.Rect.Width > IndoorsSteeringManager.smallRoomSize / 2 && tooCloseCharacter == null) + if (currentHull.Rect.Width > IndoorsSteeringManager.smallRoomSize / 2 && tooCloseCharacter == null) { foreach (Character c in Character.CharacterList) { @@ -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) { @@ -487,7 +495,7 @@ namespace Barotrauma foreach (Item item in Item.ItemList) { if (item.CurrentHull != hull) { continue; } - if (AIObjectiveCleanupItems.IsValidTarget(item, character) && !ignoredItems.Contains(item)) + if (AIObjectiveCleanupItems.IsValidTarget(item, character, checkInventory: true) && !ignoredItems.Contains(item)) { itemsToClean.Add(item); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs index a31401560..a710e81c9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs @@ -139,13 +139,13 @@ namespace Barotrauma } else { - if (objectiveManager.CurrentOrder == this) + if (objectiveManager.IsOrder(this)) { - Priority = ForceOrderPriority ? AIObjectiveManager.OrderPriority : targetValue; + Priority = ForceOrderPriority ? objectiveManager.GetOrderPriority(this) : targetValue; } else { - float max = MathHelper.Min(AIObjectiveManager.OrderPriority - 1, 90); + float max = AIObjectiveManager.LowestOrderPriority - 1; float value = MathHelper.Clamp((CumulatedDevotion + (targetValue * PriorityModifier)) / 100, 0, 1); Priority = MathHelper.Lerp(0, max, value); } @@ -167,7 +167,7 @@ namespace Barotrauma foreach (T target in GetList()) { // The bots always find targets when the objective is an order. - if (objectiveManager.CurrentOrder != this) + if (!objectiveManager.IsOrder(this)) { // Battery or pump states cannot currently be reported (not implemented) and therefore we must ignore them -> the bots always know if they require attention. bool ignore = this is AIObjectiveChargeBatteries || this is AIObjectivePumpWater; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs index 56b5c4ac8..391461cec 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs @@ -1,6 +1,6 @@ using Barotrauma.Extensions; using Barotrauma.Items.Components; -using Barotrauma.Networking; +using Barotrauma.Networking; // used by the server using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -10,8 +10,8 @@ namespace Barotrauma { class AIObjectiveManager { - // TODO: expose - public const float OrderPriority = 70; + public const float HighestOrderPriority = 70; + public const float LowestOrderPriority = 60; public const float RunPriority = 50; // Constantly increases the priority of the selected objective, unless overridden public const float baseDevotion = 5; @@ -25,7 +25,6 @@ namespace Barotrauma public HumanAIController HumanAIController => character.AIController as HumanAIController; - private float _waitTimer; /// /// When set above zero, the character will stand still doing nothing until the timer runs out. Does not affect orders, find safety or combat. @@ -39,26 +38,25 @@ namespace Barotrauma } } - public AIObjective CurrentOrder { get; private set; } + public List CurrentOrders { get; } = new List(); + /// + /// The AIObjective in with the highest + /// + public AIObjective CurrentOrder + { + get + { + return ForcedOrder ?? currentOrder; + } + private set + { + currentOrder = value; + } + } + private AIObjective currentOrder; + public AIObjective ForcedOrder { get; private set; } public AIObjective CurrentObjective { get; private set; } - public bool IsCurrentOrder() where T : AIObjective => CurrentOrder is T; - public bool IsCurrentObjective() where T : AIObjective => CurrentObjective is T; - public bool IsActiveObjective() where T : AIObjective => GetActiveObjective() is T; - - public AIObjective GetActiveObjective() => CurrentObjective?.GetActiveObjective(); - /// - /// Returns the last active objective of the specific type. - /// - public T GetActiveObjective() where T : AIObjective => CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).LastOrDefault(so => so is T) as T; - - /// - /// Returns all active objectives of the specific type. Creates a new collection -> don't use too frequently. - /// - public IEnumerable GetActiveObjectives() where T : AIObjective => CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).Where(so => so is T).Select(so => so as T); - - public bool HasActiveObjective() where T : AIObjective => CurrentObjective is T || CurrentObjective != null && CurrentObjective.GetSubObjectivesRecursive().Any(so => so is T); - public AIObjectiveManager(Character character) { this.character = character; @@ -127,10 +125,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) { @@ -196,21 +198,34 @@ namespace Barotrauma public void UpdateObjectives(float deltaTime) { - if (CurrentOrder != null) + UpdateOrderObjective(ForcedOrder); + + if (CurrentOrders.Any()) { + foreach(var order in CurrentOrders) + { + var orderObjective = order.Objective; + UpdateOrderObjective(orderObjective); + } + } + + void UpdateOrderObjective(AIObjective orderObjective) + { + if (orderObjective == null) { return; } #if DEBUG // Note: don't automatically remove orders here. Removing orders needs to be done via dismissing. - if (CurrentOrder.IsCompleted) + if (orderObjective.IsCompleted) { - DebugConsole.NewMessage($"{character.Name}: ORDER {CurrentOrder.DebugTag} IS COMPLETED. CURRENTLY ALL ORDERS SHOULD BE LOOPING.", Color.Red); + DebugConsole.NewMessage($"{character.Name}: ORDER {orderObjective.DebugTag} IS COMPLETED. CURRENTLY ALL ORDERS SHOULD BE LOOPING.", Color.Red); } - else if (!CurrentOrder.CanBeCompleted) + else if (!orderObjective.CanBeCompleted) { - DebugConsole.NewMessage($"{character.Name}: ORDER {CurrentOrder.DebugTag}, CANNOT BE COMPLETED.", Color.Red); + DebugConsole.NewMessage($"{character.Name}: ORDER {orderObjective.DebugTag}, CANNOT BE COMPLETED.", Color.Red); } #endif - CurrentOrder.Update(deltaTime); + orderObjective.Update(deltaTime); } + if (WaitTimer > 0) { WaitTimer -= deltaTime; @@ -244,7 +259,29 @@ namespace Barotrauma public void SortObjectives() { - CurrentOrder?.GetPriority(); + ForcedOrder?.GetPriority(); + + AIObjective orderWithHighestPriority = null; + float highestPriority = 0; + foreach (var currentOrder in CurrentOrders) + { + var orderObjective = currentOrder.Objective; + if (orderObjective == null) { continue; } + orderObjective.GetPriority(); + if (orderWithHighestPriority == null || orderObjective.Priority > highestPriority) + { + orderWithHighestPriority = orderObjective; + highestPriority = orderObjective.Priority; + } + } +#if SERVER + if (orderWithHighestPriority != null && orderWithHighestPriority != currentOrder) + { + GameMain.NetworkMember.CreateEntityEvent(character, new object[] { NetEntityEvent.Type.ObjectiveManagerOrderState }); + } +#endif + CurrentOrder = orderWithHighestPriority; + for (int i = Objectives.Count - 1; i >= 0; i--) { Objectives[i].GetPriority(); @@ -253,6 +290,7 @@ namespace Barotrauma { Objectives.Sort((x, y) => y.Priority.CompareTo(x.Priority)); } + GetCurrentObjective()?.SortSubObjectives(); } @@ -268,12 +306,18 @@ namespace Barotrauma } } - public void SetOrder(AIObjective objective) + public void SetForcedOrder(AIObjective objective) { - CurrentOrder = objective; + ForcedOrder = objective; } - public void SetOrder(Order order, string option, Character orderGiver) + public void ClearForcedOrder() + { + ForcedOrder = null; + } + + private CoroutineHandle speakRoutine; + public void SetOrder(Order order, string option, int priority, Character orderGiver, bool speak) { if (character.IsDead) { @@ -284,8 +328,53 @@ namespace Barotrauma #endif } ClearIgnored(); - CurrentOrder = CreateObjective(order, option, orderGiver, isAutonomous: false); - if (CurrentOrder == null) + + if (order == null || order.Identifier == "dismissed") + { + if (!string.IsNullOrEmpty(option)) + { + if (CurrentOrders.Any(o => o.MatchesDismissedOrder(option))) + { + var dismissedOrderInfo = CurrentOrders.First(o => o.MatchesDismissedOrder(option)); + CurrentOrders.Remove(dismissedOrderInfo); + } + } + else + { + CurrentOrders.Clear(); + } + } + + // Make sure the order priorities reflect those set by the player + for (int i = CurrentOrders.Count - 1; i >= 0; i--) + { + var currentOrder = CurrentOrders[i]; + if (currentOrder.Objective == null || currentOrder.MatchesOrder(order, option)) + { + CurrentOrders.RemoveAt(i); + continue; + } + var currentOrderInfo = character.GetCurrentOrder(currentOrder.Order, currentOrder.OrderOption); + if (currentOrderInfo.HasValue) + { + int currentPriority = currentOrderInfo.Value.ManualPriority; + if (currentOrder.ManualPriority != currentPriority) + { + CurrentOrders[i] = new OrderInfo(currentOrder, currentPriority); + } + } + else + { + CurrentOrders.RemoveAt(i); + } + } + + var newCurrentOrder = CreateObjective(order, option, orderGiver, isAutonomous: false); + if (newCurrentOrder != null) + { + CurrentOrders.Add(new OrderInfo(order, option, priority, newCurrentOrder)); + } + if (!HasOrders()) { // Recreate objectives, because some of them may be removed, if impossible to complete (e.g. due to path finding) CreateAutonomousObjectives(); @@ -293,13 +382,57 @@ namespace Barotrauma else { // This should be redundant, because all the objectives are reset when they are selected as active. - CurrentOrder.Reset(); + newCurrentOrder?.Reset(); + + if (speak && character.IsOnPlayerTeam) + { + 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 (newCurrentOrder != null && character.SpeechImpediment < 100.0f) + // { + // if (newCurrentOrder is AIObjectiveRepairItems repairItems && repairItems.Targets.None()) + // { + // character.Speak(TextManager.Get("DialogNoRepairTargets"), null, 3.0f, "norepairtargets"); + // } + // else if (newCurrentOrder is AIObjectiveChargeBatteries chargeBatteries && chargeBatteries.Targets.None()) + // { + // character.Speak(TextManager.Get("DialogNoBatteries"), null, 3.0f, "nobatteries"); + // } + // else if (newCurrentOrder is AIObjectiveExtinguishFires extinguishFires && extinguishFires.Targets.None()) + // { + // character.Speak(TextManager.Get("DialogNoFire"), null, 3.0f, "nofire"); + // } + // else if (newCurrentOrder is AIObjectiveFixLeaks fixLeaks && fixLeaks.Targets.None()) + // { + // character.Speak(TextManager.Get("DialogNoLeaks"), null, 3.0f, "noleaks"); + // } + // else if (newCurrentOrder is AIObjectiveFightIntruders fightIntruders && fightIntruders.Targets.None()) + // { + // character.Speak(TextManager.Get("DialogNoEnemies"), null, 3.0f, "noenemies"); + // } + // else if (newCurrentOrder is AIObjectiveRescueAll rescueAll && rescueAll.Targets.None()) + // { + // character.Speak(TextManager.Get("DialogNoRescueTargets"), null, 3.0f, "norescuetargets"); + // } + // else if (newCurrentOrder is AIObjectivePumpWater pumpWater && pumpWater.Targets.None()) + // { + // character.Speak(TextManager.Get("DialogNoPumps"), null, 3.0f, "nopumps"); + // } + // } + //}, 3); + } } } public AIObjective CreateObjective(Order order, string option, Character orderGiver, bool isAutonomous, float priorityModifier = 1) { - if (order == null) { return null; } + if (order == null || order.Identifier == "dismissed") { return null; } AIObjective newObjective; switch (order.Identifier.ToLowerInvariant()) { @@ -320,8 +453,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 +477,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 +502,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) { @@ -383,7 +515,7 @@ namespace Barotrauma newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option, false, priorityModifier: priorityModifier) { IsLoop = false, - Override = character.CurrentOrder != null, + Override = !character.IsDismissed, completionCondition = () => { if (float.TryParse(option, out float pct)) @@ -403,11 +535,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) { @@ -421,21 +568,9 @@ namespace Barotrauma return newObjective; } - private void DismissSelf() - { -#if CLIENT - if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.IsSinglePlayer) - { - GameMain.GameSession?.CrewManager?.SetCharacterOrder(character, Order.GetPrefab("dismissed"), null, character); - } -#else - GameMain.Server?.SendOrderChatMessage(new OrderChatMessage(Order.GetPrefab("dismissed"), null, null, character, character)); -#endif - } - private bool IsAllowedToWait() { - if (CurrentOrder != null) { return false; } + if (HasOrders()) { return false; } if (CurrentObjective is AIObjectiveCombat || CurrentObjective is AIObjectiveFindSafety) { return false; } if (character.AnimController.InWater) { return false; } if (character.IsClimbing) { return false; } @@ -446,5 +581,61 @@ namespace Barotrauma if (AIObjectiveIdle.IsForbidden(character.CurrentHull)) { return false; } return true; } + + public bool IsCurrentOrder() where T : AIObjective => CurrentOrder is T; + public bool IsCurrentObjective() where T : AIObjective => CurrentObjective is T; + public bool IsActiveObjective() where T : AIObjective => GetActiveObjective() is T; + + public AIObjective GetActiveObjective() => CurrentObjective?.GetActiveObjective(); + /// + /// Returns the last active objective of the specific type. + /// + public T GetActiveObjective() where T : AIObjective => CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).LastOrDefault(so => so is T) as T; + + /// + /// Returns all active objectives of the specific type. Creates a new collection -> don't use too frequently. + /// + public IEnumerable GetActiveObjectives() where T : AIObjective => CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).Where(so => so is T).Select(so => so as T); + + public bool HasActiveObjective() where T : AIObjective => CurrentObjective is T || CurrentObjective != null && CurrentObjective.GetSubObjectivesRecursive().Any(so => so is T); + + public bool IsOrder(AIObjective objective) + { + return objective == ForcedOrder || CurrentOrders.Any(o => o.Objective == objective); + } + + public bool HasOrders() + { + return ForcedOrder != null || CurrentOrders.Any(); + } + + public bool HasOrder() where T : AIObjective + { + return ForcedOrder is T || CurrentOrders.Any(o => o.Objective is T); + } + + public float GetOrderPriority(AIObjective objective) + { + if (objective == ForcedOrder) { return HighestOrderPriority; } + var currentOrder = CurrentOrders.FirstOrDefault(o => o.Objective == objective); + if (currentOrder.Objective == null) + { + return HighestOrderPriority; + } + else if (currentOrder.ManualPriority > 0) + { + return MathHelper.Lerp(LowestOrderPriority, HighestOrderPriority, MathUtils.InverseLerp(1, CharacterInfo.HighestManualOrderPriority, currentOrder.ManualPriority)); + } +#if DEBUG + DebugConsole.AddWarning("Error in order priority: shouldn't return 0!"); +#endif + return 0; + } + + public OrderInfo? GetCurrentOrderInfo() + { + if (currentOrder == null) { return null; } + return CurrentOrders.FirstOrDefault(o => o.Objective == CurrentOrder); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs index 0a9f3505d..9a6824696 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs @@ -36,7 +36,7 @@ namespace Barotrauma public override float GetPriority() { - bool isOrder = objectiveManager.CurrentOrder == this; + bool isOrder = objectiveManager.IsOrder(this); if (!IsAllowed || character.LockHands) { Priority = 0; @@ -51,7 +51,7 @@ namespace Barotrauma { if (isOrder) { - Priority = AIObjectiveManager.OrderPriority; + Priority = objectiveManager.GetOrderPriority(this); } ItemComponent target = GetTarget(); Item targetItem = target?.Item; @@ -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)) { @@ -89,11 +89,16 @@ namespace Barotrauma case "powerup": // Check that we don't already have another order that is targeting the same item. // Without this the autonomous objective will tell the bot to turn the reactor on again. - if (objectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder != this && operateOrder.GetTarget() == target && operateOrder.Option != Option) + + if (IsAnotherOrderTargetingSameItem(objectiveManager.ForcedOrder) || objectiveManager.CurrentOrders.Any(o => IsAnotherOrderTargetingSameItem(o.Objective))) { Priority = 0; return Priority; } + bool IsAnotherOrderTargetingSameItem(AIObjective objective) + { + return objective is AIObjectiveOperateItem operateObjective && operateObjective != this && operateObjective.GetTarget() == target && operateObjective.Option != Option; + } break; } } @@ -101,20 +106,30 @@ 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; } else { - float value = CumulatedDevotion + (AIObjectiveManager.OrderPriority * PriorityModifier); - float max = isOrder ? MathHelper.Min(AIObjectiveManager.OrderPriority, 90) : AIObjectiveManager.RunPriority - 1; - if (!isOrder && reactor != null && reactor.PowerOn && Option == "powerup") + if (isOrder) { - // Decrease the priority when targeting a reactor that is already on. - value /= 2; + float max = objectiveManager.GetOrderPriority(this); + float value = CumulatedDevotion + (max * PriorityModifier); + Priority = MathHelper.Clamp(value, 0, max); + } + else + { + float value = CumulatedDevotion + (AIObjectiveManager.LowestOrderPriority * PriorityModifier); + float max = AIObjectiveManager.LowestOrderPriority - 1; + if (reactor != null && reactor.PowerOn && Option == "powerup") + { + // Decrease the priority when targeting a reactor that is already on. + value /= 2; + } + Priority = MathHelper.Clamp(value, 0, max); } - Priority = MathHelper.Clamp(value, 0, max); } } return Priority; @@ -137,7 +152,7 @@ namespace Barotrauma throw new Exception("target null"); #endif } - else if (target.Item.NonInteractable) + else if (!target.Item.IsInteractable(character)) { Abandon = true; } @@ -153,25 +168,13 @@ namespace Barotrauma ItemComponent target = GetTarget(); if (useController && controller == null) { - character.Speak(TextManager.GetWithVariable("DialogCantFindController", "[item]", component.Item.Name, true), null, 2.0f, "cantfindcontroller", 30.0f); + if (character.IsOnPlayerTeam) + { + character.Speak(TextManager.GetWithVariable("DialogCantFindController", "[item]", component.Item.Name, true), null, 2.0f, "cantfindcontroller", 30.0f); + } 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 +218,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 +244,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..ab3574641 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? @@ -55,13 +59,13 @@ namespace Barotrauma float dist = Math.Abs(character.WorldPosition.X - Item.WorldPosition.X) + yDist; distanceFactor = MathHelper.Lerp(1, 0.25f, MathUtils.InverseLerp(0, 4000, dist)); } - float requiredSuccessFactor = objectiveManager.IsCurrentOrder() ? 0 : AIObjectiveRepairItems.RequiredSuccessFactor; + float requiredSuccessFactor = objectiveManager.HasOrder() ? 0 : AIObjectiveRepairItems.RequiredSuccessFactor; float severity = isPriority ? 1 : AIObjectiveRepairItems.GetTargetPriority(Item, character, requiredSuccessFactor) / 100; bool isSelected = IsRepairing(); float selectedBonus = isSelected ? 100 - MaxDevotion : 0; float devotion = (CumulatedDevotion + selectedBonus) / 100; float reduction = isPriority ? 1 : isSelected ? 2 : 3; - float max = MathHelper.Min(AIObjectiveManager.OrderPriority - reduction, 90); + float max = AIObjectiveManager.LowestOrderPriority - reduction; Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1)); } return Priority; @@ -70,7 +74,7 @@ namespace Barotrauma protected override bool Check() { IsCompleted = Item.IsFullCondition; - if (IsCompleted && IsRepairing()) + if (character.IsOnPlayerTeam && IsCompleted && IsRepairing()) { character.Speak(TextManager.GetWithVariable("DialogItemRepaired", "[itemname]", Item.Name, true), null, 0.0f, "itemrepaired", 10.0f); } @@ -93,7 +97,10 @@ namespace Barotrauma var getItemObjective = new AIObjectiveGetItem(character, requiredItem.Identifiers, objectiveManager, true); if (objectiveManager.IsCurrentOrder()) { - getItemObjective.Abandoned += () => character.Speak(TextManager.Get("dialogcannotfindrequireditemtorepair"), null, 0.0f, "dialogcannotfindrequireditemtorepair", 10.0f); + if (character.IsOnPlayerTeam) + { + getItemObjective.Abandoned += () => character.Speak(TextManager.Get("dialogcannotfindrequireditemtorepair"), null, 0.0f, "dialogcannotfindrequireditemtorepair", 10.0f); + } } subObjectives.Add(getItemObjective); } @@ -107,8 +114,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 +122,20 @@ namespace Barotrauma Abandon = true; return; } - // Drop empty tanks - foreach (Item containedItem in containedItems) - { - if (containedItem == null) { continue; } - if (containedItem.Condition <= 0.0f) - { - containedItem.Drop(character); - } - } + // Eject empty tanks + HumanAIController.UnequipEmptyItems(repairTool.Item); 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; @@ -178,7 +177,7 @@ namespace Barotrauma } if (Abandon) { - if (IsRepairing()) + if (character.IsOnPlayerTeam && IsRepairing()) { character.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, true), null, 0.0f, "cannotrepair", 10.0f); } @@ -213,7 +212,7 @@ namespace Barotrauma onAbandon: () => { Abandon = true; - if (IsRepairing()) + if (character.IsOnPlayerTeam && IsRepairing()) { character.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, true), null, 0.0f, "cannotrepair", 10.0f); } @@ -229,7 +228,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..01fcadd83 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs @@ -104,7 +104,7 @@ namespace Barotrauma } bool anyFixers = otherFixers > 0; float ratio = anyFixers ? items / (float)otherFixers : 1; - if (objectiveManager.CurrentOrder == this) + if (objectiveManager.IsOrder(this)) { return Targets.Sum(t => 100 - t.ConditionPercentage); } @@ -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..23010dc8c 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. @@ -322,7 +322,7 @@ namespace Barotrauma { itemListStr = string.Join(" or ", string.Join(", ", itemNameList.Take(itemNameList.Count - 1)), itemNameList.Last()); } - if (targetCharacter != character) + if (targetCharacter != character && character.IsOnPlayerTeam) { character.Speak(TextManager.GetWithVariables("DialogListRequiredTreatments", new string[2] { "[targetname]", "[treatmentlist]" }, new string[2] { targetCharacter.Name, itemListStr }, new bool[2] { false, true }), @@ -331,9 +331,16 @@ 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; + if (character != targetCharacter && character.IsOnPlayerTeam) + { + character.Speak(TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, formatCapitals: false), identifier: "cannottreatpatient", minDurationBetweenSimilar: 20.0f); + } + }); } } } @@ -380,7 +387,7 @@ namespace Barotrauma return false; } bool isCompleted = AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, targetCharacter); - if (isCompleted && targetCharacter != character) + if (isCompleted && targetCharacter != character && character.IsOnPlayerTeam) { character.Speak(TextManager.GetWithVariable("DialogTargetHealed", "[targetname]", targetCharacter.Name), null, 1.0f, "targethealed" + targetCharacter.Name, 60.0f); @@ -427,6 +434,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..9cef0c85e 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; } } @@ -37,7 +40,7 @@ namespace Barotrauma protected override float TargetEvaluation() { if (Targets.None()) { return 100; } - if (objectiveManager.CurrentOrder != this) + if (!objectiveManager.IsOrder(this)) { if (!character.IsMedic && HumanAIController.IsTrueForAnyCrewMember(c => c != HumanAIController && c.Character.IsMedic && !c.Character.IsUnconscious)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs index 923924892..fd318964e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs @@ -19,27 +19,62 @@ namespace Barotrauma struct OrderInfo { - public string ComponentIdentifier { get; set; } - public Order Order { get; private set; } - public string OrderOption { get; private set; } + public Order Order { get; } + public string OrderOption { get; } + public int ManualPriority { get; } + public OrderType Type { get; } + public AIObjective Objective { get; } + public bool IsCurrentOrder => Type == OrderType.Current; - public OrderInfo(Order order, string orderOption) + public enum OrderType + { + Current, + Previous + } + + private OrderInfo(Order order, string orderOption, int manualPriority, OrderType orderType, AIObjective objective) { - ComponentIdentifier = "currentorder"; Order = order; OrderOption = orderOption; + ManualPriority = Math.Min(manualPriority, CharacterInfo.HighestManualOrderPriority); + Type = orderType; + Objective = objective; } - public OrderInfo(OrderInfo orderInfo) - { - ComponentIdentifier = "previousorder"; - Order = orderInfo.Order; - OrderOption = orderInfo.OrderOption; - } + public OrderInfo(Order order, string orderOption, int manualPriority) : this(order, orderOption, manualPriority, OrderType.Current, null) { } + + public OrderInfo(Order order, string orderOption, int manualPriority, AIObjective objective) : this(order, orderOption, manualPriority, OrderType.Current, objective) { } + + public OrderInfo(OrderInfo orderInfo, int manualPriority) : this(orderInfo.Order, orderInfo.OrderOption, manualPriority, orderInfo.Type, orderInfo.Objective) { } + + public OrderInfo(OrderInfo orderInfo, OrderType type) : this(orderInfo.Order, orderInfo.OrderOption, orderInfo.ManualPriority, type, orderInfo.Objective) { } + + public bool MatchesOrder(string orderIdentifier, string orderOption) => + (orderIdentifier == Order?.Identifier || (string.IsNullOrEmpty(orderIdentifier) && string.IsNullOrEmpty(Order?.Identifier))) && + (orderOption == OrderOption || (string.IsNullOrEmpty(orderOption) && string.IsNullOrEmpty(OrderOption))); public bool MatchesOrder(Order order, string option) => - order.Identifier == Order.Identifier && - option == OrderOption; + MatchesOrder(order?.Identifier, option); + + public bool MatchesOrder(OrderInfo orderInfo) => + MatchesOrder(orderInfo.Order?.Identifier, orderInfo.OrderOption); + + public bool MatchesDismissedOrder(string dismissOrderOption) + { + string[] dismissedOrder = dismissOrderOption?.Split('.'); + if (dismissedOrder != null && dismissedOrder.Length > 0) + { + string dismissedOrderIdentifier = dismissedOrder.Length > 0 ? dismissedOrder[0] : null; + if (dismissedOrderIdentifier == null || dismissedOrderIdentifier != Order?.Identifier) { return false; } + string dismissedOrderOption = dismissedOrder.Length > 1 ? dismissedOrder[1] : null; + if (dismissedOrderOption == null && string.IsNullOrEmpty(OrderOption)) { return true; } + return dismissedOrderOption == OrderOption; + } + else + { + return false; + } + } } class Order @@ -59,6 +94,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 +136,6 @@ namespace Barotrauma public bool TargetAllCharacters { get; } public bool IsReport => TargetAllCharacters && !MustSetTarget; - public readonly float FadeOutTime; public Entity TargetEntity; @@ -119,9 +157,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 +197,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 +282,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 +311,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 +360,7 @@ namespace Barotrauma IsPrefab = true; MustManuallyAssign = orderElement.GetAttributeBool("mustmanuallyassign", false); IsIgnoreOrder = Identifier == "ignorethis" || Identifier == "unignorethis"; + DrawIconWhenContained = orderElement.GetAttributeBool("displayiconwhencontained", false); } /// @@ -324,23 +370,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 +400,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; } } @@ -400,7 +447,7 @@ namespace Barotrauma orderOption ??= ""; string messageTag = (givingOrderToSelf && !TargetAllCharacters ? "OrderDialogSelf." : "OrderDialog.") + Identifier; - if (!string.IsNullOrEmpty(orderOption)) { messageTag += "." + orderOption; } + if (Identifier != "dismissed" && !string.IsNullOrEmpty(orderOption)) { messageTag += "." + orderOption; } if (targetCharacterName == null) { targetCharacterName = ""; } if (targetRoomName == null) { targetRoomName = ""; } @@ -433,7 +480,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 +504,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) @@ -478,5 +533,23 @@ namespace Barotrauma if (index < 0 || index >= Options.Length) { return null; } return GetOptionName(Options[index]); } + + /// + /// Used to create the order option for the Dismiss order to know which order it targets + /// + /// The order to target with the dismiss order + public static string GetDismissOrderOption(OrderInfo orderInfo) + { + if (orderInfo.Order != null) + { + string option = orderInfo.Order.Identifier; + if (!string.IsNullOrEmpty(orderInfo.OrderOption)) + { + option += $".{orderInfo.OrderOption}"; + } + return option; + } + return ""; + } } } 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/SteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringManager.cs index 290a8a3d0..780ec45e4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringManager.cs @@ -94,20 +94,24 @@ namespace Barotrauma { Vector2 targetVel = target - host.SimPosition; - if (targetVel.LengthSquared() < 0.00001f) return Vector2.Zero; + if (targetVel.LengthSquared() < 0.00001f) { return Vector2.Zero; } targetVel = Vector2.Normalize(targetVel) * weight; - Vector2 newSteering = targetVel - host.Steering; + // TODO: the code below doesn't quite work as it should, and I'm not sure what the purpose of it is/was. + // So, we'll just return the targetVel for now, as it produces smooth results. + return targetVel; - if (newSteering == Vector2.Zero) return Vector2.Zero; + //Vector2 newSteering = targetVel - host.Steering; - float steeringSpeed = (newSteering + host.Steering).Length(); - if (steeringSpeed > Math.Abs(weight)) - { - newSteering = Vector2.Normalize(newSteering) * Math.Abs(weight); - } + //if (newSteering == Vector2.Zero) return Vector2.Zero; - return newSteering; + //float steeringSpeed = (newSteering + host.Steering).Length(); + //if (steeringSpeed > Math.Abs(weight)) + //{ + // newSteering = Vector2.Normalize(newSteering) * Math.Abs(weight); + //} + + //return newSteering; } protected virtual Vector2 DoSteeringWander(float weight) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs index 69976c41d..2f961341e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs @@ -35,7 +35,7 @@ namespace Barotrauma private static IEnumerable GetThalamusEntities(Submarine wreck, string tag) => MapEntity.mapEntityList.Where(e => e.Submarine == wreck && e.prefab != null && IsThalamus(e.prefab, tag)); - private static bool IsThalamus(MapEntityPrefab entityPrefab, string tag) => entityPrefab.Category == MapEntityCategory.Thalamus || entityPrefab.Tags.Contains(tag); + private static bool IsThalamus(MapEntityPrefab entityPrefab, string tag) => entityPrefab.HasSubCategory("thalamus") || entityPrefab.Tags.Contains(tag); public WreckAI(Submarine wreck) { @@ -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) { @@ -246,7 +246,7 @@ namespace Barotrauma initialCellsSpawned = true; } - private void Kill() + public void Kill() { thalamusItems.ForEach(i => i.Condition = 0); foreach (var turret in turrets) @@ -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 579a658e2..6904391ca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs @@ -1,18 +1,9 @@ using Microsoft.Xna.Framework; -using System; namespace Barotrauma { partial class AICharacter : Character - { - //characters that are further than this from the camera (and all clients) - //have all their limb physics bodies disabled - const float EnableSimplePhysicsDist = 6000.0f; - const float DisableSimplePhysicsDist = EnableSimplePhysicsDist * 0.9f; - - const float EnableSimplePhysicsDistSqr = EnableSimplePhysicsDist * EnableSimplePhysicsDist; - const float DisableSimplePhysicsDistSqr = DisableSimplePhysicsDist * DisableSimplePhysicsDist; - + { private AIController aiController; public override AIController AIController @@ -20,8 +11,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, ushort id = Entity.NullEntityID, bool isNetworkPlayer = false, RagdollParams ragdoll = null) + : base(prefab, speciesName, position, seed, characterInfo, id: id, isRemotePlayer: isNetworkPlayer, ragdollParams: ragdoll) { InitProjSpecific(); } @@ -62,21 +53,12 @@ namespace Barotrauma if (!IsRemotePlayer && !(AIController is HumanAIController)) { - float characterDist = float.MaxValue; -#if CLIENT - characterDist = Vector2.DistanceSquared(cam.GetPosition(), WorldPosition); -#elif SERVER - if (GameMain.Server != null) - { - characterDist = GetClosestDistance(); - } -#endif - - if (characterDist > EnableSimplePhysicsDistSqr) + float characterDistSqr = GetDistanceSqrToClosestPlayer(); + if (characterDistSqr > MathUtils.Pow2(Params.DisableDistance * 0.5f)) { AnimController.SimplePhysicsEnabled = true; } - else if (characterDist < DisableSimplePhysicsDistSqr) + else if (characterDistSqr < MathUtils.Pow2(Params.DisableDistance * 0.5f * 0.9f)) { AnimController.SimplePhysicsEnabled = false; } @@ -90,50 +72,5 @@ namespace Barotrauma aiController.Update(deltaTime); } } - -#if SERVER - // Gets the closest distance, either an active player character or spectator - private float GetClosestDistance() - { - float minDist = float.MaxValue; - - for (int i = 0; i < GameMain.Server.ConnectedClients.Count; i++) - { - var spectatePos = GameMain.Server.ConnectedClients[i].SpectatePos; - if (spectatePos != null) - { - float dist = Vector2.DistanceSquared(spectatePos.Value, WorldPosition); - - if (dist < minDist) - { - minDist = dist; - } - if (dist < DisableSimplePhysicsDistSqr) - { - return dist; - } - } - } - - foreach (Character c in CharacterList) - { - if (c != this && c.IsRemotePlayer) - { - float dist = Vector2.DistanceSquared(c.WorldPosition, WorldPosition); - - if (dist < minDist) - { - minDist = dist; - } - if (dist < DisableSimplePhysicsDistSqr) - { - return dist; - } - } - } - - return minDist; - } -#endif } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs index b08df6dab..3e940af24 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) @@ -408,23 +423,26 @@ namespace Barotrauma if (CurrentSwimParams == null) { return; } movement = TargetMovement; bool isMoving = movement.LengthSquared() > 0.00001f; + var mainLimb = MainLimb; if (isMoving) { float t = 0.5f; - if (CurrentSwimParams.RotateTowardsMovement && VectorExtensions.Angle(VectorExtensions.Forward(Collider.Rotation + MathHelper.PiOver2), movement) > MathHelper.PiOver2) + if (!SimplePhysicsEnabled && CurrentSwimParams.RotateTowardsMovement) { - // Reduce the linear movement speed when not facing the movement direction - t /= 5; + float offset = mainLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2; + Vector2 forward = VectorExtensions.Forward(mainLimb.body.TransformedRotation - offset * Character.AnimController.Dir); + float dot = Vector2.Dot(forward, Vector2.Normalize(movement)); + if (dot < 0) + { + // Reduce the linear movement speed when not facing the movement direction + t = MathHelper.Clamp((1 + dot) / 10, 0.01f, 0.1f); + } } Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, t); } - //limbs are disabled when simple physics is enabled, no need to move them if (SimplePhysicsEnabled) { return; } - var mainLimb = MainLimb; mainLimb.PullJointEnabled = true; - //mainLimb.PullJointWorldAnchorB = Collider.SimPosition; - if (!isMoving) { WalkPos = MathHelper.SmoothStep(WalkPos, MathHelper.PiOver2, deltaTime * 5); @@ -630,7 +648,7 @@ namespace Barotrauma } if (limb.Params.BlinkFrequency > 0) { - limb.Blink(deltaTime, MainLimb.Rotation); + limb.UpdateBlink(deltaTime, MainLimb.Rotation); } } @@ -772,7 +790,7 @@ namespace Barotrauma } if (limb.Params.BlinkFrequency > 0) { - limb.Blink(deltaTime, MainLimb.Rotation); + limb.UpdateBlink(deltaTime, MainLimb.Rotation); } switch (limb.type) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index b42f8dfa1..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(); } @@ -1315,16 +1317,23 @@ namespace Barotrauma var thigh = i == 0 ? GetLimb(LimbType.LeftThigh) : GetLimb(LimbType.RightThigh); if (thigh == null) { continue; } if (thigh.IsSevered) { continue; } - float thighDiff = Math.Abs(MathUtils.GetShortestAngle(torso.Rotation, thigh.Rotation)); - float thighTorque = thighDiff * thigh.Mass * Math.Sign(torso.Rotation - thigh.Rotation) * 5.0f; - thigh.body.ApplyTorque(thighTorque * strength); + float diff = torso.Rotation - thigh.Rotation; + if (MathUtils.IsValid(diff)) + { + float thighTorque = thighDiff * thigh.Mass * Math.Sign(diff) * 5.0f; + thigh.body.ApplyTorque(thighTorque * strength); + } var leg = i == 0 ? GetLimb(LimbType.LeftLeg) : GetLimb(LimbType.RightLeg); if (leg == null || leg.IsSevered) { continue; } float legDiff = Math.Abs(MathUtils.GetShortestAngle(torso.Rotation, leg.Rotation)); - float legTorque = legDiff * leg.Mass * Math.Sign(torso.Rotation - leg.Rotation) * 5.0f; - leg.body.ApplyTorque(legTorque * strength); + diff = torso.Rotation - leg.Rotation; + if (MathUtils.IsValid(diff)) + { + float legTorque = legDiff * leg.Mass * Math.Sign(diff) * 5.0f; + leg.body.ApplyTorque(legTorque * strength); + } } } @@ -1452,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 @@ -1460,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); } } } @@ -1764,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]; @@ -1779,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; @@ -1798,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]; @@ -1846,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]); } } } @@ -2025,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 11f3f3f3a..f3049f3e7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -281,7 +281,7 @@ namespace Barotrauma } } - public const float MAX_SPEED = 15; + public const float MAX_SPEED = 30; public Vector2 TargetMovement { @@ -472,7 +472,7 @@ namespace Barotrauma if (joint == null) { continue; } float angle = (joint.LowerLimit + joint.UpperLimit) / 2.0f; joint.LimbB?.body?.SetTransform( - (joint.WorldAnchorA - MathUtils.RotatePointAroundTarget(joint.LocalAnchorB, Vector2.Zero, MathHelper.ToDegrees(joint.BodyA.Rotation + angle), true)), + (joint.WorldAnchorA - MathUtils.RotatePointAroundTarget(joint.LocalAnchorB, Vector2.Zero, joint.BodyA.Rotation + angle, true)), joint.BodyA.Rotation + angle); } } @@ -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); } } @@ -1120,6 +1120,32 @@ namespace Barotrauma splashSoundTimer -= deltaTime; + if (character.Submarine == null && Level.Loaded != null) + { + if (Collider.SimPosition.Y > Level.Loaded.TopBarrier.Position.Y) + { + Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X, Math.Min(Collider.LinearVelocity.Y, -1)); + } + else if (Collider.SimPosition.Y < Level.Loaded.BottomBarrier.Position.Y) + { + Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X, + MathHelper.Clamp(Collider.LinearVelocity.Y, Level.Loaded.BottomBarrier.Position.Y - Collider.SimPosition.Y, 10.0f)); + } + foreach (Limb limb in Limbs) + { + if (limb.SimPosition.Y > Level.Loaded.TopBarrier.Position.Y) + { + limb.body.LinearVelocity = new Vector2(limb.LinearVelocity.X, Math.Min(limb.LinearVelocity.Y, -1)); + } + else if (limb.SimPosition.Y < Level.Loaded.BottomBarrier.Position.Y) + { + limb.body.LinearVelocity = new Vector2( + limb.LinearVelocity.X, + MathHelper.Clamp(limb.LinearVelocity.Y, Level.Loaded.BottomBarrier.Position.Y - limb.SimPosition.Y, 10.0f)); + } + } + } + if (forceStanding) { inWater = false; @@ -1562,7 +1588,7 @@ namespace Barotrauma } } - public void SetPosition(Vector2 simPosition, bool lerp = false, bool ignorePlatforms = true) + public void SetPosition(Vector2 simPosition, bool lerp = false, bool ignorePlatforms = true, bool forceMainLimbToCollider = false) { if (!MathUtils.IsValid(simPosition)) { @@ -1575,8 +1601,7 @@ namespace Barotrauma } if (MainLimb == null) { return; } - Vector2 limbMoveAmount = simPosition - Collider.SimPosition; - + Vector2 limbMoveAmount = forceMainLimbToCollider ? simPosition - MainLimb.SimPosition : simPosition - Collider.SimPosition; if (lerp) { Collider.TargetPosition = simPosition; @@ -1587,13 +1612,15 @@ namespace Barotrauma Collider.SetTransform(simPosition, Collider.Rotation); } - foreach (Limb limb in Limbs) + if (!MathUtils.NearlyEqual(limbMoveAmount, Vector2.Zero)) { - if (limb.IsSevered) { continue; } - //check visibility from the new position of the collider to the new position of this limb - Vector2 movePos = limb.SimPosition + limbMoveAmount; - - TrySetLimbPosition(limb, simPosition, movePos, lerp, ignorePlatforms); + foreach (Limb limb in Limbs) + { + if (limb.IsSevered) { continue; } + //check visibility from the new position of the collider to the new position of this limb + Vector2 movePos = limb.SimPosition + limbMoveAmount; + TrySetLimbPosition(limb, simPosition, movePos, lerp, ignorePlatforms); + } } } @@ -1644,7 +1671,7 @@ namespace Barotrauma if (distSqrd > resetDist * resetDist) { //ragdoll way too far, reset position - SetPosition(Collider.SimPosition, true); + SetPosition(Collider.SimPosition, true, forceMainLimbToCollider: true); } if (distSqrd > allowedDist * allowedDist) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index 5b2607e9d..247b898b4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -121,11 +121,29 @@ 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 =>_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 ItemDamage { get; set; } + public float LevelWallDamage { get; set; } [Serialize(false, true)] public bool Ranged { get; set; } @@ -199,6 +217,9 @@ namespace Barotrauma [Serialize("0.0, 0.0", true, description: "Applied to the target, in world space coordinates(i.e. 0, -1 pushes the target downwards). The attacker's facing direction is taken into account."), Editable] public Vector2 TargetForceWorld { get; private set; } + [Serialize(1.0f, true, description: "Affects the strength of the impact effects the limb causes when it hits a submarine."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] + public float SubmarineImpactMultiplier { get; private set; } + [Serialize(0.0f, true, description: "How likely the attack causes target limbs to be severed."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] public float SeverLimbsProbability { get; set; } @@ -210,6 +231,9 @@ namespace Barotrauma [Serialize(0.0f, true, description: ""), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] public float Priority { get; private set; } + [Serialize(false, true, description: ""), Editable] + public bool Blink { get; private set; } + public IEnumerable StatusEffects { get { return statusEffects; } @@ -260,6 +284,11 @@ namespace Barotrauma return (Duration == 0.0f) ? StructureDamage : StructureDamage * deltaTime; } + public float GetLevelWallDamage(float deltaTime) + { + return (Duration == 0.0f) ? LevelWallDamage : LevelWallDamage * deltaTime; + } + public float GetItemDamage(float deltaTime) { return (Duration == 0.0f) ? ItemDamage : ItemDamage * deltaTime; @@ -272,7 +301,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) @@ -283,7 +312,7 @@ namespace Barotrauma Range = range; DamageRange = range; - StructureDamage = structureDamage; + StructureDamage = LevelWallDamage = structureDamage; ItemDamage = itemDamage; } @@ -299,6 +328,13 @@ namespace Barotrauma DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Define damage as afflictions instead of using the damage attribute (e.g. )."); } + //if level wall damage is not defined, default to the structure damage + if (element.Attribute("LevelWallDamage") == null && + element.Attribute("levelwalldamage") == null) + { + LevelWallDamage = StructureDamage; + } + InitProjSpecific(element); foreach (XElement subElement in element.Elements()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index ce95510de..f05e16ba6 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; @@ -189,31 +207,8 @@ namespace Barotrauma private float attackCoolDown; - public Order CurrentOrder - { - get - { - return Info?.CurrentOrder; - } - private set - { - if (Info != null) { Info.CurrentOrder = value; } - } - } - - public string CurrentOrderOption - { - get - { - return Info?.CurrentOrderOption; - } - private set - { - if (Info != null) { Info.CurrentOrderOption = value; } - } - } - - public bool IsDismissed => Info != null && Info.IsDismissed; + public List CurrentOrders => Info?.CurrentOrders; + public bool IsDismissed => !GetCurrentOrderWithTopPriority().HasValue; private readonly List statusEffects = new List(); @@ -260,6 +255,8 @@ namespace Barotrauma } } + public string VariantOf { get; private set; } + public string Name { get @@ -429,6 +426,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 +456,7 @@ namespace Barotrauma } set { - obstructVisionAmount = 1.0f; + obstructVisionAmount = value ? 1.0f : 0.0f; } } @@ -459,6 +470,9 @@ namespace Barotrauma } } + public const float KnockbackCooldown = 30.0f; + public float KnockbackCooldownTimer; + private float ragdollingLockTimer; public bool IsRagdolled; public bool IsForceRagdolled; @@ -498,6 +512,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 +591,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 +639,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 +701,7 @@ namespace Barotrauma } else { - return (IsDead || Stun > 0.0f || LockHands || IsIncapacitated); + return IsDead || Stun > 0.0f || LockHands || IsIncapacitated; } } set { canInventoryBeAccessed = value; } @@ -680,6 +715,8 @@ namespace Barotrauma } } + public bool InWater => AnimController?.InWater ?? false; + public bool GodMode = false; public CampaignMode.InteractionType CampaignInteractionType; @@ -770,7 +807,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 +817,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, id, 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, id, 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, isRemotePlayer, ragdoll); } float healthRegen = newCharacter.Params.Health.ConstantHealthRegeneration; @@ -833,16 +871,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 +887,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 +917,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 +945,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 +988,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 +1011,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 +1245,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 +1642,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 +1792,7 @@ namespace Barotrauma var door = item.GetComponent(); if (door != null) { - return !door.IsOpen && !door.IsBroken; + return !door.CanBeTraversed; } } return false; @@ -1739,7 +1819,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 +1834,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 +1844,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 +1887,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 +1898,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 +1918,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) @@ -1911,9 +1955,9 @@ namespace Barotrauma return checkVisibility ? CanSeeCharacter(c) : true; } - public bool CanInteractWith(Item item) + public bool CanInteractWith(Item item, bool checkLinked = true) { - return CanInteractWith(item, out _, checkLinked: true); + return CanInteractWith(item, out _, checkLinked); } public bool CanInteractWith(Item item, out float distanceToItem, bool checkLinked) @@ -1924,7 +1968,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) { @@ -2003,7 +2047,7 @@ namespace Barotrauma distanceToItem = Vector2.Distance(rectIntersectionPoint, playerDistanceCheckPosition); } - if (distanceToItem > item.InteractDistance && item.InteractDistance > 0.0f) return false; + if (distanceToItem > item.InteractDistance && item.InteractDistance > 0.0f) { return false; } if (!item.Prefab.InteractThroughWalls && Screen.Selected != GameMain.SubEditorScreen && !insideTrigger) { @@ -2024,8 +2068,8 @@ namespace Barotrauma itemPosition += item.Submarine.SimPosition; itemPosition -= Submarine.SimPosition; } - var body = Submarine.CheckVisibility(SimPosition, itemPosition, true); - if (body != null && body.UserData as Item != item) return false; + var body = Submarine.CheckVisibility(SimPosition, itemPosition, ignoreLevel: true); + if (body != null && body.UserData as Item != item) { return false; } } return true; @@ -2115,7 +2159,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; @@ -2272,7 +2316,7 @@ namespace Barotrauma else { float closestPlayerDist = c.GetDistanceToClosestPlayer(); - if (closestPlayerDist > NetConfig.DisableCharacterDist) + if (closestPlayerDist > c.Params.DisableDistance) { c.Enabled = false; if (c.IsDead && c.AIController is EnemyAIController) @@ -2280,7 +2324,7 @@ namespace Barotrauma Spawner?.AddToRemoveQueue(c); } } - else if (closestPlayerDist < NetConfig.EnableCharacterDist) + else if (closestPlayerDist < c.Params.DisableDistance * 0.9f) { c.Enabled = true; } @@ -2299,7 +2343,7 @@ namespace Barotrauma distSqr = Math.Min(distSqr, Vector2.DistanceSquared(GameMain.GameScreen.Cam.GetPosition(), c.WorldPosition)); } - if (distSqr > NetConfig.DisableCharacterDistSqr) + if (distSqr > MathUtils.Pow2(c.Params.DisableDistance)) { c.Enabled = false; if (c.IsDead && c.AIController is EnemyAIController) @@ -2307,7 +2351,7 @@ namespace Barotrauma Entity.Spawner?.AddToRemoveQueue(c); } } - else if (distSqr < NetConfig.EnableCharacterDistSqr) + else if (distSqr < MathUtils.Pow2(c.Params.DisableDistance * 0.9f)) { c.Enabled = true; } @@ -2325,6 +2369,8 @@ namespace Barotrauma { UpdateProjSpecific(deltaTime, cam); + KnockbackCooldownTimer -= deltaTime; + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && this == Controlled && !isSynced) { return; } UpdateDespawn(deltaTime); @@ -2350,10 +2396,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 +2406,11 @@ namespace Barotrauma HideFace = false; - UpdateSightRange(deltaTime); UpdateSoundRange(deltaTime); + UpdateAttackers(deltaTime); + if (IsDead) { return; } if (GameMain.NetworkMember != null) @@ -2521,7 +2567,57 @@ namespace Barotrauma partial void UpdateProjSpecific(float deltaTime, Camera cam); - partial void SetOrderProjSpecific(Order order, string orderOption); + partial void SetOrderProjSpecific(Order order, string orderOption, int priority); + + + 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) { @@ -2545,7 +2641,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,21 +2650,31 @@ namespace Barotrauma } OxygenAvailable += MathHelper.Clamp(hullAvailableOxygen - oxygenAvailable, -deltaTime * 50.0f, deltaTime * 50.0f); } - + UseHullOxygen = true; } - partial void UpdateOxygenProjSpecific(float prevOxygen); - /// /// How far the character is from the closest human player (including spectators) /// - private float GetDistanceToClosestPlayer() + protected float GetDistanceToClosestPlayer() + { + return (float)Math.Sqrt(GetDistanceSqrToClosestPlayer()); + } + + /// + /// How far the character is from the closest human player (including spectators) + /// + protected float GetDistanceSqrToClosestPlayer() { float distSqr = float.MaxValue; foreach (Character otherCharacter in CharacterList) { if (otherCharacter == this || !otherCharacter.IsRemotePlayer) { continue; } distSqr = Math.Min(distSqr, Vector2.DistanceSquared(otherCharacter.WorldPosition, WorldPosition)); + if (otherCharacter.ViewTarget != null) + { + distSqr = Math.Min(distSqr, Vector2.DistanceSquared(otherCharacter.ViewTarget.WorldPosition, WorldPosition)); + } } #if SERVER for (int i = 0; i < GameMain.Server.ConnectedClients.Count; i++) @@ -2587,7 +2693,7 @@ namespace Barotrauma } distSqr = Math.Min(distSqr, Vector2.DistanceSquared(GameMain.GameScreen.Cam.Position, WorldPosition)); #endif - return (float)Math.Sqrt(distSqr); + return distSqr; } private float despawnTimer; @@ -2615,7 +2721,7 @@ namespace Barotrauma } float distToClosestPlayer = GetDistanceToClosestPlayer(); - if (distToClosestPlayer > NetConfig.DisableCharacterDist) + if (distToClosestPlayer > Params.DisableDistance) { //despawn in 1 minute if very far from all human players despawnTimer = Math.Max(despawnTimer, GameMain.Config.CorpseDespawnDelay - 60.0f); @@ -2652,18 +2758,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 @@ -2741,7 +2845,7 @@ namespace Barotrauma return !string.IsNullOrEmpty(ChatMessage.ApplyDistanceEffect("message", messageType, speaker, this)); } - public void SetOrder(Order order, string orderOption, Character orderGiver, bool speak = true) + public void SetOrder(Order order, string orderOption, int priority, Character orderGiver, bool speak = true) { //set the character order only if the character is close enough to hear the message if (orderGiver != null && !CanHearCharacter(orderGiver)) { return; } @@ -2749,25 +2853,137 @@ namespace Barotrauma // If there's another character operating the same device, make them dismiss themself if (order != null && order.Category == OrderCategory.Operate && order.TargetEntity != null) { - CharacterList.FindAll(c => c != this && c.TeamID == TeamID && c.CurrentOrder is Order characterOrder && characterOrder.Category == OrderCategory.Operate && - characterOrder.Identifier.Equals(order.Identifier) && characterOrder.TargetEntity == order.TargetEntity)? - .ForEach(c => c.SetOrder(Order.GetPrefab("dismissed"), null, c, speak: true)); + foreach (var character in CharacterList) + { + if (character == this) { continue; } + if (character.TeamID != TeamID) { continue; } + foreach (var currentOrder in character.CurrentOrders) + { + if (currentOrder.Order == null) { continue; } + if (currentOrder.Order.Category != OrderCategory.Operate) { continue; } + if (currentOrder.Order.Identifier != order.Identifier) { continue; } + if (currentOrder.Order.TargetEntity != order.TargetEntity) { continue; } + character.SetOrder(Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(currentOrder), currentOrder.ManualPriority, character); + break; + } + } } + // Prevent adding duplicate orders (same identifier and same option) + RemoveDuplicateOrders(order, orderOption); + + OrderInfo newOrderInfo = new OrderInfo(order, orderOption, priority); + AddCurrentOrder(newOrderInfo); if (AIController is HumanAIController humanAI) { - humanAI.SetOrder(order, orderOption, orderGiver, speak); + humanAI.SetOrder(order, orderOption, priority, orderGiver, speak); } - - SetOrderProjSpecific(order, orderOption); - CurrentOrder = order; - CurrentOrderOption = orderOption; + SetOrderProjSpecific(order, orderOption, priority); } - /// - /// Reset order data so it doesn't carry into further rounds, as the AI is "recreated" always in between rounds anyway. - /// - public void ResetCurrentOrder() => Info?.ResetCurrentOrder(); + private void AddCurrentOrder(OrderInfo newOrder) + { + if (newOrder.Order == null || newOrder.Order.Identifier == "dismissed") + { + if (!string.IsNullOrEmpty(newOrder.OrderOption)) + { + if (CurrentOrders.Any(o => o.MatchesDismissedOrder(newOrder.OrderOption))) + { + var dismissedOrderInfo = CurrentOrders.First(o => o.MatchesDismissedOrder(newOrder.OrderOption)); + int dismissedOrderPriority = dismissedOrderInfo.ManualPriority; + CurrentOrders.Remove(dismissedOrderInfo); + for (int i = 0; i < CurrentOrders.Count; i++) + { + var orderInfo = CurrentOrders[i]; + if (orderInfo.ManualPriority < dismissedOrderPriority) + { + CurrentOrders[i] = new OrderInfo(orderInfo, orderInfo.ManualPriority + 1); + } + } + } + } + else + { + CurrentOrders.Clear(); + } + } + else + { + for (int i = 0; i < CurrentOrders.Count; i++) + { + var orderInfo = CurrentOrders[i]; + if (orderInfo.ManualPriority <= newOrder.ManualPriority) + { + CurrentOrders[i] = new OrderInfo(orderInfo, orderInfo.ManualPriority - 1); + } + } + CurrentOrders.RemoveAll(order => order.ManualPriority <= 0); + CurrentOrders.Add(newOrder); + // Sort the current orders so the one with the highest priority comes first + CurrentOrders.Sort((x, y) => y.ManualPriority.CompareTo(x.ManualPriority)); + } + } + + private void RemoveDuplicateOrders(Order order, string option) + { + int? priorityOfRemoved = null; + for (int i = CurrentOrders.Count - 1; i >= 0; i--) + { + var orderInfo = CurrentOrders[i]; + if (orderInfo.MatchesOrder(order, option)) + { + priorityOfRemoved = orderInfo.ManualPriority; + CurrentOrders.RemoveAt(i); + break; + } + } + + if (!priorityOfRemoved.HasValue) { return; } + + for (int i = 0; i < CurrentOrders.Count; i++) + { + var orderInfo = CurrentOrders[i]; + if (orderInfo.ManualPriority < priorityOfRemoved.Value) + { + CurrentOrders[i] = new OrderInfo(orderInfo, orderInfo.ManualPriority + 1); + } + } + + CurrentOrders.RemoveAll(order => order.ManualPriority <= 0); + // Sort the current orders so the one with the highest priority comes first + CurrentOrders.Sort((x, y) => y.ManualPriority.CompareTo(x.ManualPriority)); + } + + public OrderInfo? GetCurrentOrderWithTopPriority() + { + return GetCurrentOrder(orderInfo => + { + if (orderInfo.Order == null) { return false; } + if (orderInfo.Order.Identifier == "dismissed") { return false; } + if (orderInfo.ManualPriority < 1) { return false; } + return true; + }); + } + + public OrderInfo? GetCurrentOrder(Order order, string option) + { + return GetCurrentOrder(orderInfo => + { + return orderInfo.MatchesOrder(order, option); + }); + } + + private OrderInfo? GetCurrentOrder(Func predicate) + { + if (CurrentOrders != null && CurrentOrders.Any(predicate)) + { + return CurrentOrders.First(predicate); + } + else + { + return null; + } + } private readonly List aiChatMessageQueue = new List(); @@ -2898,8 +3114,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; @@ -2977,7 +3193,6 @@ namespace Barotrauma otherLimb.body.ApplyLinearImpulse(targetLimb.LinearVelocity * targetLimb.Mass, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.5f); ApplyStatusEffects(ActionType.OnSevered, 1.0f); targetLimb.ApplyStatusEffects(ActionType.OnSevered, 1.0f); - otherLimb.ApplyStatusEffects(ActionType.OnSevered, 1.0f); } } if (wasSevered && targetLimb.character.AIController is EnemyAIController enemyAI) @@ -2991,7 +3206,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; @@ -3013,10 +3228,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(); } @@ -3060,7 +3291,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) { @@ -3069,6 +3300,10 @@ namespace Barotrauma if (!wasDead) { TryAdjustAttackerSkill(attacker, -attackResult.Damage); + if (IsDead) + { + attacker?.RecordKill(this); + } } }; if (attackResult.Damage > 0) @@ -3077,7 +3312,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; @@ -3097,7 +3334,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) @@ -3105,7 +3342,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); } } @@ -3164,6 +3401,12 @@ namespace Barotrauma Limb limb = AnimController.GetLimb(limbType); statusEffect.Apply(actionType, deltaTime, this, limb); } + else if (statusEffect.HasTargetType(StatusEffect.TargetType.LastLimb)) + { + // Target just the last matching limb + Limb limb = AnimController.Limbs.LastOrDefault(l => l.type == limbType && !l.IsSevered && !l.Hidden); + statusEffect.Apply(actionType, deltaTime, this, limb); + } } } } @@ -3244,7 +3487,7 @@ namespace Barotrauma GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.Status }); } - IsDead = true; + isDead = true; ApplyStatusEffects(ActionType.OnDeath, 1.0f); @@ -3284,9 +3527,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; @@ -3316,16 +3559,18 @@ namespace Barotrauma return; } - IsDead = false; - if (aiTarget != null) { aiTarget.Remove(); } aiTarget = new AITarget(this); - SetAllDamage(0.0f, 0.0f, 0.0f); CharacterHealth.RemoveAllAfflictions(); + SetAllDamage(0.0f, 0.0f, 0.0f); + Oxygen = 100.0f; + Bloodloss = 0.0f; + SetStun(0.0f, true); + isDead = false; foreach (LimbJoint joint in AnimController.LimbJoints) { @@ -3364,10 +3609,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); @@ -3379,12 +3626,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); } } @@ -3412,18 +3656,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()) @@ -3437,10 +3676,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()) { @@ -3466,28 +3705,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; + } + } } } @@ -3497,13 +3799,12 @@ 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++; } } } - private readonly HashSet currentContexts = new HashSet(); public IEnumerable GetAttackContexts() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index aa4e3ce3a..be7848329 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,13 +345,13 @@ namespace Barotrauma public CauseOfDeath CauseOfDeath; - public Character.TeamType TeamID; + public CharacterTeamType TeamID; private readonly NPCPersonalityTrait personalityTrait; - public Order CurrentOrder { get; set; } - public string CurrentOrderOption { get; set; } - public bool IsDismissed => CurrentOrder == null || CurrentOrder.Identifier.Equals("dismissed", StringComparison.OrdinalIgnoreCase); + public const int MaxCurrentOrders = 3; + public static int HighestManualOrderPriority => MaxCurrentOrders; + public List CurrentOrders { get; } = new List(); //unique ID given to character infos in MP //used by clients to identify which infos are the same to prevent duplicate characters in round summary @@ -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); } } @@ -1013,13 +1004,9 @@ namespace Barotrauma faceAttachments = null; } - /// - /// Reset order data so it doesn't carry into further rounds, as the AI is "recreated" always in between rounds anyway. - /// - public void ResetCurrentOrder() + public void ClearCurrentOrders() { - CurrentOrder = null; - CurrentOrderOption = ""; + CurrentOrders.Clear(); } public void Remove() 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..2bc993c17 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) @@ -98,7 +102,7 @@ namespace Barotrauma private void ApplyDamage(float deltaTime, bool applyForce) { - int limbCount = character.AnimController.Limbs.Count(l => !l.IgnoreCollisions && !l.IsSevered); + int limbCount = character.AnimController.Limbs.Count(l => !l.IgnoreCollisions && !l.IsSevered && !l.Hidden); foreach (Limb limb in character.AnimController.Limbs) { if (limb.IsSevered) { continue; } @@ -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)); @@ -160,6 +176,13 @@ namespace Barotrauma private IEnumerable CreateAIHusk() { + //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)) + { + yield return CoroutineStatus.Success; + } + character.Enabled = false; Entity.Spawner.AddToRemoveQueue(character); @@ -179,7 +202,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 +224,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..117a3c108 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,9 +105,13 @@ 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 + class AfflictionPrefab : IPrefab, IDisposable, IHasUintIdentifier { public class Effect { @@ -220,6 +228,7 @@ namespace Barotrauma public static AfflictionPrefab Bloodloss; public static AfflictionPrefab Pressure; public static AfflictionPrefab Stun; + public static AfflictionPrefab RadiationSickness; public static readonly PrefabCollection Prefabs = new PrefabCollection(); @@ -248,7 +257,7 @@ namespace Barotrauma /// Unique identifier that's generated by hashing the prefab's string identifier. /// Used to reduce the amount of bytes needed to write affliction data into network messages in multiplayer. /// - public uint UIntIdentifier; + public uint UIntIdentifier { get; set; } // Arbitrary string that is used to identify the type of the affliction. public readonly string AfflictionType; @@ -265,6 +274,7 @@ namespace Barotrauma public ContentPackage ContentPackage { get; private set; } public readonly string Name, Description; + public readonly string TranslationOverride; public readonly bool IsBuff; public readonly string CauseOfDeathDescription, SelfCauseOfDeathDescription; @@ -329,6 +339,7 @@ namespace Barotrauma Bloodloss = null; Pressure = null; Stun = null; + RadiationSickness = null; #if CLIENT CharacterHealth.DamageOverlay?.Remove(); CharacterHealth.DamageOverlay = null; @@ -353,6 +364,7 @@ namespace Barotrauma if (Bloodloss == null) { DebugConsole.ThrowError("Affliction \"Bloodloss\" not defined in the affliction prefabs."); } if (Pressure == null) { DebugConsole.ThrowError("Affliction \"Pressure\" not defined in the affliction prefabs."); } if (Stun == null) { DebugConsole.ThrowError("Affliction \"Stun\" not defined in the affliction prefabs."); } + if (RadiationSickness == null) { DebugConsole.ThrowError("Affliction \"RadiationSickness\" not defined in the affliction prefabs."); } } public static void LoadFromFile(ContentFile file) @@ -490,26 +502,16 @@ namespace Barotrauma case "stun": Stun = prefab; break; + case "radiationsickness": + RadiationSickness = prefab; + break; } if (ImpactDamage == null) { ImpactDamage = InternalDamage; } if (prefab != null) { Prefabs.Add(prefab, isOverride); - } - } - - using MD5 md5 = MD5.Create(); - foreach (AfflictionPrefab prefab in Prefabs) - { - prefab.UIntIdentifier = ToolBox.StringToUInt32Hash(prefab.Identifier, md5); - - //it's theoretically possible for two different values to generate the same hash, but the probability is astronomically small - var collision = Prefabs.Find(p => p != prefab && p.UIntIdentifier == prefab.UIntIdentifier); - if (collision != null) - { - DebugConsole.ThrowError("Hashing collision when generating uint identifiers for Afflictions: " + prefab.Identifier + " has the same identifier as " + collision.Identifier + " (" + prefab.UIntIdentifier + ")"); - collision.UIntIdentifier++; + prefab.CalculatePrefabUIntIdentifier(Prefabs); } } } @@ -541,8 +543,10 @@ namespace Barotrauma Identifier = element.GetAttributeString("identifier", ""); AfflictionType = element.GetAttributeString("type", ""); - Name = TextManager.Get("AfflictionName." + Identifier, true) ?? element.GetAttributeString("name", ""); - Description = TextManager.Get("AfflictionDescription." + Identifier, true) ?? element.GetAttributeString("description", ""); + TranslationOverride = element.GetAttributeString("translationoverride", null); + string translationId = TranslationOverride ?? Identifier; + Name = TextManager.Get("AfflictionName." + translationId, true) ?? element.GetAttributeString("name", ""); + Description = TextManager.Get("AfflictionDescription." + translationId, true) ?? element.GetAttributeString("description", ""); IsBuff = element.GetAttributeBool("isbuff", false); LimbSpecific = element.GetAttributeBool("limbspecific", false); @@ -567,12 +571,12 @@ namespace Barotrauma KarmaChangeOnApplied = element.GetAttributeFloat("karmachangeonapplied", 0.0f); - CauseOfDeathDescription = TextManager.Get("AfflictionCauseOfDeath." + Identifier, true) ?? element.GetAttributeString("causeofdeathdescription", ""); - SelfCauseOfDeathDescription = TextManager.Get("AfflictionCauseOfDeathSelf." + Identifier, true) ?? element.GetAttributeString("selfcauseofdeathdescription", ""); + CauseOfDeathDescription = TextManager.Get("AfflictionCauseOfDeath." + translationId, true) ?? element.GetAttributeString("causeofdeathdescription", ""); + SelfCauseOfDeathDescription = TextManager.Get("AfflictionCauseOfDeathSelf." + translationId, true) ?? element.GetAttributeString("selfcauseofdeathdescription", ""); 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..f94e72bb5 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)); @@ -685,12 +686,12 @@ namespace Barotrauma for (int j = limbHealths[i].Afflictions.Count - 1; j >= 0; j--) { var affliction = limbHealths[i].Afflictions[j]; - Limb targetLimb = Character.AnimController.Limbs.FirstOrDefault(l => l.HealthIndex == i); + Limb targetLimb = Character.AnimController.Limbs.LastOrDefault(l => !l.IsSevered && !l.Hidden && l.HealthIndex == i); affliction.Update(this, targetLimb, deltaTime); affliction.DamagePerSecondTimer += deltaTime; - if (affliction is AfflictionBleeding) + if (affliction is AfflictionBleeding bleeding) { - UpdateBleedingProjSpecific((AfflictionBleeding)affliction, targetLimb, deltaTime); + UpdateBleedingProjSpecific(bleeding, targetLimb, deltaTime); } Character.StackSpeedMultiplier(affliction.GetSpeedMultiplier()); } 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/HumanPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs index bcfeaa2fe..f2928903f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs @@ -20,6 +20,12 @@ namespace Barotrauma [Serialize(1f, false)] public float HealthMultiplier { get; protected set; } + [Serialize(1f, false)] + public float AimSpeed { get; protected set; } + + [Serialize(1f, false)] + public float AimAccuracy { get; protected set; } + private readonly HashSet moduleFlags = new HashSet(); [Serialize("", true, "What outpost module tags does the NPC prefer to spawn in.")] @@ -67,6 +73,9 @@ namespace Barotrauma [Serialize(AIObjectiveIdle.BehaviorType.Passive, false)] public AIObjectiveIdle.BehaviorType Behavior { get; protected set; } + [Serialize(float.PositiveInfinity, false)] + public float ReportRange { get; protected set; } + public List PreferredOutpostModuleTypes { get; protected set; } public string OriginalName { get { return Identifier; } } @@ -105,16 +114,50 @@ namespace Barotrauma return Job != null && Job != "any" ? JobPrefab.Get(Job) : JobPrefab.Random(randSync); } - public void GiveItems(Character character, Submarine submarine, Rand.RandSync randSync = Rand.RandSync.Unsynced) + public void InitializeCharacter(Character npc, ISpatialEntity positionToStayIn = null) + { + npc.CharacterHealth.MaxVitality *= HealthMultiplier; + var humanAI = npc.AIController as HumanAIController; + if (humanAI != null) + { + var idleObjective = humanAI.ObjectiveManager.GetObjective(); + if (positionToStayIn != null && Behavior == AIObjectiveIdle.BehaviorType.StayInHull) + { + idleObjective.TargetHull = AIObjectiveGoTo.GetTargetHull(positionToStayIn); + idleObjective.Behavior = AIObjectiveIdle.BehaviorType.StayInHull; + } + else + { + idleObjective.Behavior = Behavior; + foreach (string moduleType in PreferredOutpostModuleTypes) + { + idleObjective.PreferredOutpostModuleTypes.Add(moduleType); + } + } + humanAI.ReportRange = ReportRange; + humanAI.AimSpeed = AimSpeed; + humanAI.AimAccuracy = AimAccuracy; + } + if (CampaignInteractionType != CampaignMode.InteractionType.None) + { + (GameMain.GameSession.GameMode as CampaignMode)?.AssignNPCMenuInteraction(npc, CampaignInteractionType); + if (positionToStayIn != null && humanAI != null) + { + humanAI.ObjectiveManager.SetForcedOrder(new AIObjectiveGoTo(positionToStayIn, npc, humanAI.ObjectiveManager, repeat: true, getDivingGearIfNeeded: false, closeEnough: 200)); + } + } + } + + public void GiveItems(Character character, Submarine submarine, Rand.RandSync randSync = Rand.RandSync.Unsynced, bool createNetworkEvents = true) { var spawnItems = ToolBox.SelectWeightedRandom(ItemSets.Keys.ToList(), ItemSets.Values.ToList(), randSync); foreach (XElement itemElement in spawnItems.GetChildElements("item")) { - InitializeItems(character, itemElement, submarine); + InitializeItems(character, itemElement, submarine, createNetworkEvents: createNetworkEvents); } } - private void InitializeItems(Character character, XElement itemElement, Submarine submarine, Item parentItem = null) + private void InitializeItems(Character character, XElement itemElement, Submarine submarine, Item parentItem = null, bool createNetworkEvents = true) { ItemPrefab itemPrefab; string itemIdentifier = itemElement.GetAttributeString("identifier", ""); @@ -126,7 +169,7 @@ namespace Barotrauma } Item item = new Item(itemPrefab, character.Position, null); #if SERVER - if (GameMain.Server != null && Entity.Spawner != null) + if (GameMain.Server != null && Entity.Spawner != null && createNetworkEvents) { if (GameMain.Server.EntityEventManager.UniqueEvents.Any(ev => ev.Entity == item)) { @@ -187,7 +230,7 @@ namespace Barotrauma } foreach (XElement childItemElement in itemElement.Elements()) { - InitializeItems(character, childItemElement, submarine, item); + InitializeItems(character, childItemElement, submarine, item, createNetworkEvents); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index 65874e3d5..72c053579 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -203,7 +203,7 @@ namespace Barotrauma partial class Limb : ISerializableEntity, ISpatialEntity { //how long it takes for severed limbs to fade out - public float SeveredFadeOutTime => Params.SeveredFadeOutTime; + public float SeveredFadeOutTime { get; private set; } = 10; public readonly Character character; /// @@ -308,6 +308,12 @@ namespace Barotrauma set { if (isSevered == value) { return; } + if (value == true) + { + // If any of the connected limbs have a longer fade out time, use that + var connectedLimbs = GetConnectedLimbs(); + SeveredFadeOutTime = Math.Max(Params.SeveredFadeOutTime, connectedLimbs.Any() ? connectedLimbs.Max(l => l.SeveredFadeOutTime) : 0); + } isSevered = value; if (isSevered) { @@ -330,7 +336,7 @@ namespace Barotrauma } } - public Submarine Submarine => character.Submarine; + public Submarine Submarine => character?.Submarine; public bool Hidden { @@ -340,7 +346,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 +628,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 +683,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 +723,7 @@ namespace Barotrauma } } } - float finalDamageModifier = 1.0f; + float finalDamageModifier = damageMultiplier; foreach (DamageModifier damageModifier in tempModifiers) { finalDamageModifier *= damageModifier.DamageMultiplier; @@ -853,6 +867,23 @@ namespace Barotrauma float dist = distance > -1 ? distance : ConvertUnits.ToDisplayUnits(Vector2.Distance(simPos, attackSimPos)); bool wasRunning = attack.IsRunning; attack.UpdateAttackTimer(deltaTime, character); + if (attack.Blink) + { + if (attack.ForceOnLimbIndices != null && attack.ForceOnLimbIndices.Any()) + { + foreach (int limbIndex in attack.ForceOnLimbIndices) + { + if (limbIndex < 0 || limbIndex >= character.AnimController.Limbs.Length) { continue; } + Limb limb = character.AnimController.Limbs[limbIndex]; + if (limb.IsSevered) { continue; } + limb.Blink(); + } + } + else + { + Blink(); + } + } bool wasHit = false; Body structureBody = null; @@ -1095,7 +1126,7 @@ namespace Barotrauma { targets.Clear(); statusEffect.GetNearbyTargets(WorldPosition, targets); - statusEffect.Apply(ActionType.OnActive, deltaTime, character, targets); + statusEffect.Apply(actionType, deltaTime, character, targets); } else { @@ -1103,7 +1134,40 @@ namespace Barotrauma { statusEffect.Apply(actionType, deltaTime, character, character, WorldPosition); } - statusEffect.Apply(actionType, deltaTime, character, this, WorldPosition); + else if (statusEffect.targetLimbs != null) + { + foreach (var limbType in statusEffect.targetLimbs) + { + if (statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs)) + { + // Target all matching limbs + foreach (var limb in ragdoll.Limbs) + { + if (limb.IsSevered) { continue; } + if (limb.type == limbType) + { + statusEffect.Apply(actionType, deltaTime, character, limb); + } + } + } + else if (statusEffect.HasTargetType(StatusEffect.TargetType.Limb)) + { + // Target just the first matching limb + Limb limb = ragdoll.GetLimb(limbType); + statusEffect.Apply(actionType, deltaTime, character, limb); + } + else if (statusEffect.HasTargetType(StatusEffect.TargetType.LastLimb)) + { + // Target just the last matching limb + Limb limb = ragdoll.Limbs.LastOrDefault(l => l.type == limbType && !l.IsSevered && !l.Hidden); + statusEffect.Apply(actionType, deltaTime, character, limb); + } + } + } + else + { + statusEffect.Apply(actionType, deltaTime, character, this, WorldPosition); + } } } } @@ -1113,7 +1177,12 @@ namespace Barotrauma private float TotalBlinkDurationOut => Params.BlinkDurationOut + Params.BlinkHoldTime; - public void Blink(float deltaTime, float referenceRotation) + public void Blink() + { + blinkTimer = -TotalBlinkDurationOut; + } + + public void UpdateBlink(float deltaTime, float referenceRotation) { if (blinkTimer > -TotalBlinkDurationOut) { @@ -1147,6 +1216,26 @@ namespace Barotrauma } } + public IEnumerable GetConnectedJoints() => ragdoll.LimbJoints.Where(j => !j.IsSevered && (j.LimbA == this || j.LimbB == this)); + + public IEnumerable GetConnectedLimbs() + { + var connectedJoints = GetConnectedJoints(); + var connectedLimbs = new HashSet(); + foreach (Limb limb in ragdoll.Limbs) + { + var otherJoints = limb.GetConnectedJoints(); + foreach (LimbJoint connectedJoint in connectedJoints) + { + if (otherJoints.Contains(connectedJoint)) + { + connectedLimbs.Add(limb); + } + } + } + return connectedLimbs; + } + public void Remove() { body?.Remove(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs index 1d18cf6d4..34c017ebe 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs @@ -54,7 +54,7 @@ namespace Barotrauma abstract class SwimParams : AnimationParams { - [Serialize(25.0f, true, description: "Turning speed (or rather a force applied on the main collider to make it turn). Note that you can set a limb-specific steering forces too (additional)."), Editable(MinValueFloat = 0, MaxValueFloat = 500, ValueStep = 1)] + [Serialize(25.0f, true, description: "Turning speed (or rather a force applied on the main collider to make it turn). Note that you can set a limb-specific steering forces too (additional)."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] public float SteerTorque { get; set; } } @@ -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..07c3bf980 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(); @@ -173,13 +173,13 @@ namespace Barotrauma [Editable, Serialize(true, true, description: "Should the character face towards the direction it's heading.")] public bool RotateTowardsMovement { get; set; } - [Serialize(25.0f, true, description: "How much torque is used to rotate the torso to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] + [Serialize(25.0f, true, description: "How much torque is used to rotate the torso to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 2000, ValueStep = 1)] public float TorsoTorque { get; set; } - [Serialize(25.0f, true, description: "How much torque is used to rotate the head to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] + [Serialize(25.0f, true, description: "How much torque is used to rotate the head to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 2000, ValueStep = 1)] public float HeadTorque { get; set; } - [Serialize(50.0f, true, description: "How much torque is used to rotate the tail to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] + [Serialize(50.0f, true, description: "How much torque is used to rotate the tail to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 2000, ValueStep = 1)] public float TailTorque { get; set; } [Serialize(1f, true, description: "Multiplier applied based on the angle difference between the tail and the main limb. Increasing the value prevents snake-like characters from getting tangled on themselves. Default = 1 (no boost)"), Editable(MinValueFloat = 1, MaxValueFloat = 100)] 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..efde3633b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -64,6 +64,9 @@ namespace Barotrauma [Serialize("waterblood", true), Editable] public string BleedParticleWater { get; private set; } + [Serialize(1f, true), Editable] + public float BleedParticleMultiplier { get; private set; } + [Serialize(10f, true, description: "How effectively/easily the character eats other characters. Affects the forces, the amount of particles, and the time required before the target is eaten away"), Editable(MinValueFloat = 1, MaxValueFloat = 1000, ValueStep = 1)] public float EatingSpeed { get; set; } @@ -76,8 +79,13 @@ namespace Barotrauma [Serialize(0f, true), Editable] public float SonarDisruption { get; set; } + [Serialize(25000f, true, "If the character is farther than this (in pixels) from the sub and the players, it will be disabled. The halved value is used for triggering simple physics where the ragdoll is disabled and only the main collider is updated."), Editable(MinValueFloat = 10000f, MaxValueFloat = 100000f)] + public float DisableDistance { get; set; } + 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 +108,33 @@ 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()) + { + if (subSubElement.Name.ToString().Equals("item", StringComparison.OrdinalIgnoreCase)) { continue; } + var matchingSubParams = matchingParams.SubParams.FirstOrDefault(p => p.Name.Equals(subSubElement.Name.ToString(), StringComparison.OrdinalIgnoreCase)); + if (matchingSubParams != null) + { + TryLoadOverride(matchingSubParams, subSubElement, matchingSubParams.SerializableProperties); + } + } + } + } + return success; + } if (string.IsNullOrEmpty(SpeciesName) && MainElement != null) { //backwards compatibility @@ -111,6 +146,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 +218,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)) { @@ -480,23 +529,42 @@ namespace Barotrauma [Serialize(20f, true, description: "How long the creature flees before returning to normal state. When the creature sees the target or is being chased, it will always flee, if it's in the flee state."), Editable(minValue: 0f, maxValue: 100f)] public float MinFleeTime { get; private set; } - [Serialize(false, true, description: "Does the character try to break inside the sub?"), Editable()] + [Serialize(false, true, description: "Does the character try to break inside the sub?"), Editable] public bool AggressiveBoarding { get; private set; } - [Serialize(true, true, description: "Enforce aggressive behavior if the creature is spawned as a target of a monster mission."), Editable()] + [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()] + [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."), Editable] + public bool Infiltrate { get; private set; } + + [Serialize(true, true, "Is the creature allowed to navigate from and into the depths of the abyss? When enabled, the creatures will try to avoid the depths."), Editable] + public bool AvoidAbyss { get; set; } + + [Serialize(true, true, "Does the creature try to keep in the abyss? Has effect only when AvoidAbyss is false."), Editable] + public bool StayInAbyss { get; set; } + + [Serialize(0f, true, description: ""), Editable] + public float StartAggression { get; private set; } + + [Serialize(100f, true, description: ""), Editable] + public float MaxAggression { get; private set; } + + [Serialize(0f, true, description: ""), Editable] + public float AggressionCumulation { 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,11 +656,24 @@ 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, description: "Should the target be ignored if it's inside a different submarine than us? Normally only some targets are ignored when they are not inside the same sub."), Editable] + public bool IgnoreIfNotInSameSub { get; set; } + + [Serialize(false, true), Editable] + public bool IgnoreIncapacitated { get; set; } + + [Serialize(0f, true, description: "How much damage the protected target should take from an attacker before the creature starts defending it."), Editable] + public float DamageThreshold { get; private set; } + + [Serialize(AttackPattern.Straight, true), Editable] + public AttackPattern AttackPattern { get; set; } + + #region Sweep [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; } @@ -601,6 +682,21 @@ 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; } + #endregion + + #region Circle + [Serialize(5000f, true), Editable(MinValueFloat = 0f, MaxValueFloat = 20000f)] + public float CircleStartDistance { get; private set; } + + [Serialize(1f, true), Editable(MinValueFloat = 0.5f, MaxValueFloat = 2f)] + public float CircleRotationSpeed { get; private set; } + + [Serialize(5f, true), Editable(MinValueFloat = 1f, MaxValueFloat = 10f)] + public float CircleStrikeDistanceMultiplier { get; private set; } + + [Serialize(0f, true), Editable(MinValueFloat = 0f, MaxValueFloat = 50f)] + public float CircleMaxRandomOffset { get; private set; } + #endregion public TargetParams(XElement element, CharacterParams character) : base(element, 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..b3f6806dc 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 @@ -584,7 +599,7 @@ namespace Barotrauma [Serialize(0f, true, description: "Width of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] public float Width { get; set; } - [Serialize(10f, true, description: "The more the density the heavier the limb is."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] + [Serialize(10f, true, description: "The more the density the heavier the limb is."), Editable(MinValueFloat = 0, MaxValueFloat = 100, DecimalCount = 2)] public float Density { get; set; } [Serialize(false, true), Editable] 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 71ca28a0d..8ce405380 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -733,6 +733,11 @@ namespace Barotrauma if (newEvent != null) { var @event = newEvent.CreateInstance(); + if (newEvent == null) + { + NewMessage($"Could not initialize event {args[0]} because level did not meet requirements"); + return; + } GameMain.GameSession.EventManager.ActiveEvents.Add(@event); @event.Init(true); NewMessage($"Initialized event {newEvent.Identifier}", Color.Aqua); @@ -816,7 +821,7 @@ namespace Barotrauma NewMessage(Hull.EditFire ? "Fire spawning on" : "Fire spawning off", Color.White); }, isCheat: true)); - commands.Add(new Command("explosion", "explosion [range] [force] [damage] [structuredamage] [item damage] [emp strength]: Creates an explosion at the position of the cursor.", null, isCheat: true)); + commands.Add(new Command("explosion", "explosion [range] [force] [damage] [structuredamage] [item damage] [emp strength] [ballast flora strength]: Creates an explosion at the position of the cursor.", null, isCheat: true)); commands.Add(new Command("showseed|showlevelseed", "showseed: Show the seed of the current level.", (string[] args) => { @@ -827,6 +832,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)); @@ -1166,6 +1173,14 @@ namespace Barotrauma c.SetAllDamage(200.0f, 0.0f, 0.0f); } } + foreach (Hull hull in Hull.hullList) + { + hull.BallastFlora?.Kill(); + } + foreach (Submarine sub in Submarine.Loaded) + { + sub.WreckAI?.Kill(); + } }, null, isCheat: true)); commands.Add(new Command("setclientcharacter", "setclientcharacter [client name] [character name]: Gives the client control of the specified character.", null, @@ -1287,6 +1302,7 @@ namespace Barotrauma { if (item.CurrentHull != null && item.HasTag("ballast") && item.GetComponent() is { } pump) { + if (item.CurrentHull.BallastFlora != null) { continue; } pumps.Add(pump); } } @@ -1301,8 +1317,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; } @@ -1448,6 +1464,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, () => @@ -1458,6 +1496,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)); @@ -1755,7 +1794,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 @@ -1971,13 +2010,21 @@ namespace Barotrauma { if (e != null) { - error += " {" + e.Message + "}\n" + e.StackTrace.CleanupStackTrace(); + error += " {" + e.Message + "}\n"; + if (e.StackTrace != null) + { + error += e.StackTrace.CleanupStackTrace(); + } if (e.InnerException != null) { - error += "\n\nInner exception: " + e.InnerException.Message + "\n" + e.InnerException.StackTrace.CleanupStackTrace(); + error += "\n\nInner exception: " + e.InnerException.Message + "\n"; + if (e.InnerException.StackTrace != null) + { + error += e.InnerException.StackTrace.CleanupStackTrace(); ; + } } } - else if (appendStackTrace) + else if (appendStackTrace && Environment.StackTrace != null) { error += "\n" + Environment.StackTrace.CleanupStackTrace(); } 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 3b7f21e7c..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; } @@ -56,5 +59,10 @@ namespace Barotrauma { return true; } + + public virtual bool LevelMeetsRequirements() + { + return true; + } } } 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..daa5e8fd0 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; } @@ -54,7 +61,6 @@ namespace Barotrauma private Character speaker; - private OrderInfo? prevSpeakerOrder; private AIObjective prevIdleObjective, prevGotoObjective; public List Options { get; private set; } @@ -104,19 +110,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,16 +184,9 @@ 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) - { - humanAI.SetOrder(prevSpeakerOrder.Value.Order, prevSpeakerOrder.Value.OrderOption, orderGiver: null, speak: false); - } - else - { - humanAI.SetOrder(null, string.Empty, orderGiver: null, speak: false); - } + humanAI.ClearForcedOrder(); if (prevIdleObjective != null) { humanAI.ObjectiveManager.AddObjective(prevIdleObjective); } if (prevGotoObjective != null) { humanAI.ObjectiveManager.AddObjective(prevGotoObjective); } } @@ -255,7 +261,12 @@ namespace Barotrauma } else { - if (Options.Any()) + if (ShouldInterrupt()) + { + ResetSpeaker(); + interrupt = true; + } + else if (Options.Any()) { Options[selectedOption].Update(deltaTime); } @@ -305,16 +316,11 @@ namespace Barotrauma if (speaker?.AIController is HumanAIController humanAI) { - prevSpeakerOrder = null; - if (humanAI.CurrentOrder != null) - { - prevSpeakerOrder = new OrderInfo(humanAI.CurrentOrder, humanAI.CurrentOrderOption); - } prevIdleObjective = humanAI.ObjectiveManager.GetObjective(); prevGotoObjective = humanAI.ObjectiveManager.GetObjective(); - humanAI.SetOrder( - Order.PrefabList.Find(o => o.Identifier.Equals("wait", StringComparison.OrdinalIgnoreCase)), - option: string.Empty, orderGiver: null, speak: false); + humanAI.SetForcedOrder( + Order.PrefabList.Find(o => o.Identifier.Equals("wait", StringComparison.OrdinalIgnoreCase)), + option: string.Empty, orderGiver: null); if (targets.Any()) { Entity closestTarget = null; @@ -335,6 +341,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 d3f99cab7..fa8733a5b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs @@ -68,6 +68,9 @@ namespace Barotrauma } } + [Serialize(false, true, description: "Should the AI ignore this item. This will prevent outpost NPCs cleaning up or otherwise using important items intended to be left for the players.")] + public bool IgnoreByAI { get; set; } + private bool spawned; private Entity spawnedEntity; @@ -106,38 +109,17 @@ 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; } - } - } - newCharacter.CharacterHealth.MaxVitality *= humanPrefab.HealthMultiplier; - var humanAI = newCharacter.AIController as HumanAIController; - if (humanAI != null) - { - var idleObjective = humanAI.ObjectiveManager.GetObjective(); - if (idleObjective != null) - { - idleObjective.Behavior = humanPrefab.Behavior; - foreach (string moduleType in humanPrefab.PreferredOutpostModuleTypes) - { - idleObjective.PreferredOutpostModuleTypes.Add(moduleType); - } - } - } - if (humanPrefab.CampaignInteractionType != CampaignMode.InteractionType.None) - { - (GameMain.GameSession.GameMode as CampaignMode)?.AssignNPCMenuInteraction(newCharacter, humanPrefab.CampaignInteractionType); - if (spawnPos != null && humanAI != null) - { - humanAI.ObjectiveManager.SetOrder(new AIObjectiveGoTo(spawnPos, newCharacter, humanAI.ObjectiveManager, repeat: true, getDivingGearIfNeeded: false, closeEnough: 200)); + item.SpawnedInOutpost = true; } } + humanPrefab.InitializeCharacter(newCharacter, spawnPos); if (!string.IsNullOrEmpty(TargetTag) && newCharacter != null) { ParentEvent.AddTarget(TargetTag, newCharacter); @@ -197,9 +179,16 @@ 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; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs index 068056bff..3a2c82772 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TagAction.cs @@ -12,6 +12,9 @@ namespace Barotrauma [Serialize("", true)] public string Tag { get; set; } + [Serialize(true, true)] + public bool IgnoreIncapacitatedCharacters { get; set; } + private bool isFinished = false; public TagAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } @@ -27,12 +30,26 @@ namespace Barotrauma private void TagPlayers() { - ParentEvent.AddTargetPredicate(Tag, e => e is Character c && c.IsPlayer); + if (IgnoreIncapacitatedCharacters) + { + ParentEvent.AddTargetPredicate(Tag, e => e is Character c && c.IsPlayer && !c.IsIncapacitated); + } + else + { + ParentEvent.AddTargetPredicate(Tag, e => e is Character c && c.IsPlayer); + } } private void TagBots() { - ParentEvent.AddTargetPredicate(Tag, e => e is Character c && c.IsBot); + if (IgnoreIncapacitatedCharacters) + { + ParentEvent.AddTargetPredicate(Tag, e => e is Character c && c.IsBot && !c.IsIncapacitated); + } + else + { + ParentEvent.AddTargetPredicate(Tag, e => e is Character c && c.IsBot); + } } private void TagCrew() 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/EventActions/TriggerEventAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerEventAction.cs index 4d9a2fc56..9a6aabf58 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerEventAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/TriggerEventAction.cs @@ -33,7 +33,11 @@ namespace Barotrauma } else { - GameMain.GameSession.EventManager.QueuedEvents.Enqueue(eventPrefab.CreateInstance()); + var ev = eventPrefab.CreateInstance(); + if (ev != null) + { + GameMain.GameSession.EventManager.QueuedEvents.Enqueue(ev); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index 78d145b41..6c9d6f5aa 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>(); @@ -86,6 +93,8 @@ namespace Barotrauma public void StartRound(Level level) { + this.level = level; + if (isClient) { return; } pendingEventSets.Clear(); @@ -100,7 +109,6 @@ namespace Barotrauma totalPathLength = steeringPath.TotalLength; } - this.level = level; SelectSettings(); var initialEventSet = SelectRandomEvents(EventSet.List); @@ -144,6 +152,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 +269,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 +343,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++) { @@ -356,7 +382,9 @@ namespace Barotrauma if (eventPrefab != null) { 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)) { @@ -378,6 +406,7 @@ namespace Barotrauma foreach (Pair eventPrefab in eventSet.EventPrefabs) { var newEvent = eventPrefab.First.CreateInstance(); + if (newEvent == null) { continue; } newEvent.Init(true); DebugConsole.Log("Initialized event " + newEvent.ToString()); if (!selectedEvents.ContainsKey(eventSet)) @@ -402,10 +431,11 @@ namespace Barotrauma var allowedEventSets = eventSets.Where(es => level.Difficulty >= es.MinLevelDifficulty && level.Difficulty <= es.MaxLevelDifficulty && level.LevelData.Type == es.LevelType); - - if (GameMain.GameSession?.GameMode is CampaignMode campaign && campaign.Map?.CurrentLocation?.Type != null) + + LocationType locationType = (GameMain.GameSession?.GameMode as CampaignMode)?.Map?.CurrentLocation?.Type ?? level?.StartLocation?.Type; + if (locationType != null) { - allowedEventSets = allowedEventSets.Where(set => set.LocationTypeIdentifiers == null || set.LocationTypeIdentifiers.Any(identifier => string.Equals(identifier, campaign.Map.CurrentLocation.Type.Identifier, StringComparison.OrdinalIgnoreCase))); + allowedEventSets = allowedEventSets.Where(set => set.LocationTypeIdentifiers == null || set.LocationTypeIdentifiers.Any(identifier => string.Equals(identifier, locationType.Identifier, StringComparison.OrdinalIgnoreCase))); } float totalCommonness = allowedEventSets.Sum(e => e.GetCommonness(level)); @@ -440,6 +470,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) { @@ -491,6 +529,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) { @@ -498,9 +555,6 @@ namespace Barotrauma calculateDistanceTraveledTimer = CalculateDistanceTraveledInterval; } - eventThreshold += settings.EventThresholdIncrease * deltaTime; - eventCoolDown -= deltaTime; - if (currentIntensity < eventThreshold) { bool recheck = false; @@ -524,7 +578,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; + } } } @@ -561,7 +618,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); @@ -584,9 +641,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))) { @@ -679,7 +735,6 @@ namespace Barotrauma } } - /// /// Finds all actions in a ScriptedEvent /// @@ -748,5 +803,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 6c64271be..b2fc0cb84 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs @@ -7,9 +7,9 @@ namespace Barotrauma class EventPrefab { public readonly XElement ConfigElement; - public readonly Type EventType; - public readonly string MusicType; + public readonly Type EventType; public readonly float SpawnProbability; + public readonly bool TriggerEventCooldown; public float Commonness; public string Identifier; @@ -17,8 +17,6 @@ namespace Barotrauma { ConfigElement = element; - MusicType = element.GetAttributeString("musictype", "default"); - try { EventType = Type.GetType("Barotrauma." + ConfigElement.Name, true, true); @@ -35,6 +33,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() @@ -50,6 +49,9 @@ namespace Barotrauma DebugConsole.ThrowError(ex.InnerException != null ? ex.InnerException.ToString() : ex.ToString()); } + Event ev = (Event)instance; + if (!ev.LevelMeetsRequirements()) { return null; } + return (Event)instance; } } 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/AbandonedOutpostMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs new file mode 100644 index 000000000..93c2caa2f --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AbandonedOutpostMission.cs @@ -0,0 +1,112 @@ +using Barotrauma.Extensions; +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Barotrauma +{ + partial class AbandonedOutpostMission : Mission + { + private readonly XElement characterConfig; + + private readonly List characters = new List(); + private readonly Dictionary> characterItems = new Dictionary>(); + + private readonly string itemTag; + + private Item itemToDestroy; + + public AbandonedOutpostMission(MissionPrefab prefab, Location[] locations) : + base(prefab, locations) + { + characterConfig = prefab.ConfigElement.Element("Characters"); + + itemTag = prefab.ConfigElement.GetAttributeString("targetitem", ""); + if (string.IsNullOrEmpty(itemTag)) + { + DebugConsole.ThrowError($"Error in mission prefab \"{prefab.Identifier}\". Target item not defined."); + } + } + + protected override void StartMissionSpecific(Level level) + { + itemToDestroy = null; + itemToDestroy = Item.ItemList.Find(it => it.Submarine?.Info.Type != SubmarineType.Player && it.HasTag(itemTag)); + if (itemToDestroy == null) + { + DebugConsole.ThrowError($"Error in mission \"{Prefab.Identifier}\". Could not find an item with the tag \"{itemTag}\"."); + } + + if (!IsClient) + { + InitCharacters(); + } + } + + private void InitCharacters() + { + characters.Clear(); + characterItems.Clear(); + + if (characterConfig == null) { return; } + + var submarine = Submarine.Loaded.Find(s => s.Info.Type == SubmarineType.Outpost) ?? Submarine.MainSub; + if (submarine.Info.Type == SubmarineType.Outpost) + { + submarine.TeamID = CharacterTeamType.None; + } + + foreach (XElement element in characterConfig.Elements()) + { + string characterIdentifier = element.GetAttributeString("identifier", ""); + string characterFrom = element.GetAttributeString("from", ""); + HumanPrefab humanPrefab = NPCSet.Get(characterFrom, characterIdentifier); + if (humanPrefab == null) + { + DebugConsole.ThrowError("Couldn't spawn character for abandoned outpost mission: character prefab \"" + characterIdentifier + "\" not found"); + return; + } + + string[] moduleFlags = element.GetAttributeStringArray("moduleflags", null); + string[] spawnPointTags = element.GetAttributeStringArray("spawnpointtags", null); + ISpatialEntity spawnPos = SpawnAction.GetSpawnPos( + SpawnAction.SpawnLocationType.Outpost, SpawnType.Human, + moduleFlags ?? humanPrefab.GetModuleFlags(), + spawnPointTags ?? humanPrefab.GetSpawnPointTags()); + if (spawnPos == null) + { + spawnPos = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandom(); + } + + var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: humanPrefab.GetJobPrefab(Rand.RandSync.Server), randSync: Rand.RandSync.Server); + Character spawnedCharacter = Character.Create(characterInfo.SpeciesName, spawnPos.WorldPosition, ToolBox.RandomSeed(8), characterInfo, createNetworkEvent: false); + spawnedCharacter.TeamID = CharacterTeamType.None; + humanPrefab.InitializeCharacter(spawnedCharacter, spawnPos); + humanPrefab.GiveItems(spawnedCharacter, Submarine.MainSub, Rand.RandSync.Server, createNetworkEvents: false); + + characters.Add(spawnedCharacter); + characterItems.Add(spawnedCharacter, spawnedCharacter.Inventory.FindAllItems(recursive: true)); + } + } + + public override void Update(float deltaTime) + { + if (State == 0 && itemToDestroy != null && itemToDestroy.Condition <= 0.0f) + { + State = 1; + } + } + + public override void End() + { + completed = itemToDestroy == null || itemToDestroy.Condition <= 0.0f; + if (completed) + { + if (Prefab.LocationTypeChangeOnCompleted != null) + { + ChangeLocationType(Prefab.LocationTypeChangeOnCompleted); + } + GiveReward(); + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs index 993129200..b3a5365f1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs @@ -1,5 +1,3 @@ -using Barotrauma.Items.Components; -using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -11,11 +9,9 @@ namespace Barotrauma partial class BeaconMission : Mission { private bool swarmSpawned; - private string monsterSpeciesName; + private readonly string monsterSpeciesName; private Point monsterCountRange; - private Level level; - private Location[] locations; - private string sonarLabel; + private readonly string sonarLabel; public BeaconMission(MissionPrefab prefab, Location[] locations) : base(prefab, locations) { @@ -34,8 +30,6 @@ namespace Barotrauma monsterCountRange = new Point(min, max); - this.locations = locations; - sonarLabel = TextManager.Get("beaconstationsonarlabel"); } @@ -51,27 +45,58 @@ namespace Barotrauma { get { - yield return level.BeaconStation.WorldPosition; + if (level.BeaconStation == null) + { + yield break; + } + yield return level.BeaconStation.WorldPosition; } } - public override void Start(Level level) - { - this.level = level; - } - public override void Update(float deltaTime) { if (IsClient) { return; } if (!swarmSpawned && level.CheckBeaconActive()) { State = 1; + Vector2 spawnPos = level.BeaconStation.WorldPosition; spawnPos.Y += level.BeaconStation.GetDockedBorders().Height * 1.5f; + + var availablePositions = Level.Loaded.PositionsOfInterest.FindAll(p => + p.PositionType == Level.PositionType.MainPath || + p.PositionType == Level.PositionType.SidePath); + availablePositions.RemoveAll(p => Level.Loaded.ExtraWalls.Any(w => w.IsPointInside(p.Position.ToVector2()))); + availablePositions.RemoveAll(p => Submarine.FindContaining(p.Position.ToVector2()) != null); + + if (availablePositions.Any()) + { + Level.InterestingPosition? closestPos = null; + float closestDist = float.PositiveInfinity; + foreach (var pos in availablePositions) + { + float dist = Vector2.DistanceSquared(pos.Position.ToVector2(), level.BeaconStation.WorldPosition); + if (dist < closestDist) + { + closestDist = dist; + closestPos = pos; + } + } + if (closestPos.HasValue) + { + spawnPos = closestPos.Value.Position.ToVector2(); + } + } + int amount = Rand.Range(monsterCountRange.X, monsterCountRange.Y + 1); for (int i = 0; i < amount; i++) { - Entity.Spawner.AddToSpawnQueue(monsterSpeciesName, spawnPos); + CoroutineManager.InvokeAfter(() => + { + //round ended before the coroutine finished + if (GameMain.GameSession == null || Level.Loaded == null) { return; } + Entity.Spawner.AddToSpawnQueue(monsterSpeciesName, spawnPos); + }, Rand.Range(0f, amount)); } swarmSpawned = true; } @@ -82,13 +107,15 @@ namespace Barotrauma completed = level.CheckBeaconActive(); if (completed) { - if (GameMain.GameSession.GameMode is CampaignMode) + if (Prefab.LocationTypeChangeOnCompleted != null) { - int naturalFormationIndex = locations[0].Type.Identifier.Equals("None", StringComparison.OrdinalIgnoreCase) ? 0 : 1; - var upgradeLocation = locations[naturalFormationIndex]; - upgradeLocation.ChangeType(LocationType.List.Find(lt => lt.Identifier.Equals("Explored", StringComparison.OrdinalIgnoreCase))); + ChangeLocationType(Prefab.LocationTypeChangeOnCompleted); } GiveReward(); + if (level?.LevelData != null) + { + level.LevelData.IsBeaconActive = true; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs index 2a1ae5069..8bb2893ba 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); @@ -115,7 +118,7 @@ namespace Barotrauma } } - public override void Start(Level level) + protected override void StartMissionSpecific(Level level) { items.Clear(); parentInventoryIDs.Clear(); @@ -135,6 +138,10 @@ namespace Barotrauma { GiveReward(); completed = true; + if (Prefab.LocationTypeChangeOnCompleted != null) + { + ChangeLocationType(Prefab.LocationTypeChangeOnCompleted); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs index efcc1c6ba..2b50cd61e 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,11 +86,11 @@ namespace Barotrauma public bool IsInWinningTeam(Character character) { return character != null && - Winner != Character.TeamType.None && + Winner != CharacterTeamType.None && Winner == character.TeamID; } - - public override void Start(Level level) + + protected override void StartMissionSpecific(Level level) { if (GameMain.NetworkMember == null) { @@ -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(); @@ -120,9 +115,9 @@ namespace Barotrauma public override void End() { - if (GameMain.NetworkMember == null) return; + 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 50a863a02..d7c0fb69c 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 @@ -42,17 +44,72 @@ namespace Barotrauma } } - public override void Start(Level level) + protected override void StartMissionSpecific(Level level) { + if (SpawnedResources.Any()) + { +#if DEBUG + throw new Exception($"SpawnedResources.Count > 0 ({SpawnedResources.Count})"); +#else + DebugConsole.AddWarning("Spawned resources list was not empty at the start of a mineral mission. The mission instance may not have been ended correctly on previous rounds."); + SpawnedResources.Clear(); +#endif + } + + if (RelevantLevelResources.Any()) + { +#if DEBUG + throw new Exception($"RelevantLevelResources.Count > 0 ({RelevantLevelResources.Count})"); +#else + DebugConsole.AddWarning("Relevant level resources list was not empty at the start of a mineral mission. The mission instance may not have been ended correctly on previous rounds."); + RelevantLevelResources.Clear(); +#endif + } + + if (MissionClusterPositions.Any()) + { +#if DEBUG + throw new Exception($"MissionClusterPositions.Count > 0 ({MissionClusterPositions.Count})"); +#else + DebugConsole.AddWarning("Mission cluster positions list was not empty at the start of a mineral mission. The mission instance may not have been ended correctly on previous rounds."); + MissionClusterPositions.Clear(); +#endif + } + + caves.Clear(); + if (IsClient) { return; } foreach (var kvp in ResourceClusters) { var prefab = ItemPrefab.Find(null, kvp.Key); - if (prefab == null) { continue; } + if (prefab == null) + { + DebugConsole.ThrowError("Error in MineralMission - " + + "couldn't find an item prefab with the identifier " + kvp.Key); + continue; + } var spawnedResources = level.GenerateMissionResources(prefab, kvp.Value.First, out float rotation); + if (spawnedResources.Count < kvp.Value.First) + { + DebugConsole.ThrowError("Error in MineralMission - " + + "spawned " + spawnedResources.Count + "/" + kvp.Value.First + " of " + prefab.Name); + } 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(); @@ -76,9 +133,29 @@ namespace Barotrauma public override void End() { - if (!EnoughHaveBeenCollected()) { return; } - GiveReward(); - completed = true; + if (EnoughHaveBeenCollected()) + { + if (Prefab.LocationTypeChangeOnCompleted != null) + { + ChangeLocationType(Prefab.LocationTypeChangeOnCompleted); + } + GiveReward(); + completed = true; + } + foreach (var kvp in SpawnedResources) + { + foreach (var i in kvp.Value) + { + if (i != null && !i.Removed && !HasBeenCollected(i)) + { + i.Remove(); + } + } + } + SpawnedResources.Clear(); + RelevantLevelResources.Clear(); + MissionClusterPositions.Clear(); + failed = !completed && state > 0; } private void FindRelevantLevelResources() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index df06e5488..c43efd9f1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -1,9 +1,7 @@ -using Barotrauma.Networking; -using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; namespace Barotrauma { @@ -11,6 +9,9 @@ namespace Barotrauma { public readonly MissionPrefab Prefab; protected bool completed, failed; + + protected Level level; + protected int state; public int State { @@ -21,7 +22,7 @@ namespace Barotrauma { state = value; #if SERVER - GameMain.Server?.UpdateMissionState(state); + GameMain.Server?.UpdateMissionState(this, state); #endif ShowMessage(State); } @@ -85,11 +86,6 @@ namespace Barotrauma get { return true; } } - public virtual int TeamCount - { - get { return 1; } - } - public virtual IEnumerable SonarPositions { get { return Enumerable.Empty(); } @@ -180,15 +176,23 @@ namespace Barotrauma return null; } - public virtual void Start(Level level) { } + public void Start(Level level) + { + foreach (string categoryToShow in Prefab.UnhideEntitySubCategories) + { + foreach (MapEntity entityToShow in MapEntity.mapEntityList.Where(me => me.prefab.HasSubCategory(categoryToShow))) + { + entityToShow.HiddenInGame = false; + } + } + this.level = level; + StartMissionSpecific(level); + } + + protected virtual void StartMissionSpecific(Level level) { } 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); @@ -202,7 +206,10 @@ namespace Barotrauma public virtual void End() { completed = true; - + if (Prefab.LocationTypeChangeOnCompleted != null) + { + ChangeLocationType(Prefab.LocationTypeChangeOnCompleted); + } GiveReward(); } @@ -234,6 +241,35 @@ namespace Barotrauma } } + protected void ChangeLocationType(LocationTypeChange change) + { + if (change == null) { throw new ArgumentException(); } + if (GameMain.GameSession.GameMode is CampaignMode && !IsClient) + { + int srcIndex = -1; + for (int i = 0; i < Locations.Length; i++) + { + if (Locations[i].Type.Identifier.Equals(change.CurrentType, StringComparison.OrdinalIgnoreCase)) + { + srcIndex = i; + break; + } + } + if (srcIndex == -1) { return; } + var location = Locations[srcIndex]; + + if (change.RequiredDurationRange.X > 0) + { + location.PendingLocationTypeChange = (change, Rand.Range(change.RequiredDurationRange.X, change.RequiredDurationRange.Y), Prefab); + } + else + { + location.ChangeType(LocationType.List.Find(lt => lt.Identifier.Equals(change.ChangeToType, StringComparison.OrdinalIgnoreCase))); + location.LocationTypeChangeCooldown = change.CooldownAfterChange; + } + } + } + public virtual void AdjustLevelData(LevelData levelData) { } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs index 63cb3de8b..49b4ae424 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs @@ -18,7 +18,9 @@ namespace Barotrauma Nest = 0x10, Mineral = 0x20, Combat = 0x40, - All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat + AbandonedOutpost = 0x80, + + All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | AbandonedOutpost } partial class MissionPrefab @@ -33,6 +35,7 @@ namespace Barotrauma { MissionType.Beacon, typeof(BeaconMission) }, { MissionType.Nest, typeof(NestMission) }, { MissionType.Mineral, typeof(MineralMission) }, + { MissionType.AbandonedOutpost, typeof(AbandonedOutpostMission) }, }; public static readonly Dictionary PvPMissionClasses = new Dictionary() { @@ -73,8 +76,26 @@ namespace Barotrauma public readonly List Headers; public readonly List Messages; - //the mission can only be received when travelling from Pair.First to Pair.Second - public readonly List> AllowedLocationTypes; + public readonly bool AllowRetry; + + public readonly bool IsSideObjective; + + /// + /// The mission can only be received when travelling from Pair.First to Pair.Second + /// + public readonly List> AllowedConnectionTypes; + + /// + /// The mission can only be received in these location types + /// + public readonly List AllowedLocationTypes = new List(); + + /// + /// Show entities belonging to these sub categories when the mission starts + /// + public readonly List UnhideEntitySubCategories = new List(); + + public LocationTypeChange LocationTypeChangeOnCompleted; public readonly XElement ConfigElement; @@ -130,7 +151,8 @@ namespace Barotrauma Name = TextManager.Get("MissionName." + TextIdentifier, true) ?? element.GetAttributeString("name", ""); Description = TextManager.Get("MissionDescription." + TextIdentifier, true) ?? element.GetAttributeString("description", ""); Reward = element.GetAttributeInt("reward", 1); - + AllowRetry = element.GetAttributeBool("allowretry", false); + IsSideObjective = element.GetAttributeBool("sideobjective", false); Commonness = element.GetAttributeInt("commonness", 1); SuccessMessage = TextManager.Get("MissionSuccess." + TextIdentifier, true) ?? element.GetAttributeString("successmessage", "Mission completed successfully"); @@ -152,9 +174,11 @@ namespace Barotrauma AchievementIdentifier = element.GetAttributeString("achievementidentifier", ""); + UnhideEntitySubCategories = element.GetAttributeStringArray("unhideentitysubcategories", new string[0]).ToList(); + Headers = new List(); Messages = new List(); - AllowedLocationTypes = new List>(); + AllowedConnectionTypes = new List>(); for (int i = 0; i < 100; i++) { @@ -183,9 +207,20 @@ namespace Barotrauma messageIndex++; break; case "locationtype": - AllowedLocationTypes.Add(new Pair( - subElement.GetAttributeString("from", ""), - subElement.GetAttributeString("to", ""))); + case "connectiontype": + if (subElement.Attribute("identifier") != null) + { + AllowedLocationTypes.Add(subElement.GetAttributeString("identifier", "")); + } + else + { + AllowedConnectionTypes.Add(new Pair( + subElement.GetAttributeString("from", ""), + subElement.GetAttributeString("to", ""))); + } + break; + case "locationtypechange": + LocationTypeChangeOnCompleted = new LocationTypeChange(subElement.GetAttributeString("from", ""), subElement, requireChangeMessages: false, defaultProbability: 1.0f); break; case "reputation": case "reputationreward": @@ -257,19 +292,32 @@ namespace Barotrauma public bool IsAllowed(Location from, Location to) { - foreach (Pair allowedLocationType in AllowedLocationTypes) + if (from == to) { - if (allowedLocationType.First.Equals("any", StringComparison.OrdinalIgnoreCase) || - allowedLocationType.First.Equals(from.Type.Identifier, StringComparison.OrdinalIgnoreCase)) + return + AllowedLocationTypes.Any(lt => lt.Equals("any", StringComparison.OrdinalIgnoreCase)) || + AllowedLocationTypes.Any(lt => lt.Equals(from.Type.Identifier, StringComparison.OrdinalIgnoreCase)); + } + + foreach (Pair allowedConnectionType in AllowedConnectionTypes) + { + if (allowedConnectionType.First.Equals("any", StringComparison.OrdinalIgnoreCase) || + allowedConnectionType.First.Equals(from.Type.Identifier, StringComparison.OrdinalIgnoreCase)) { - if (allowedLocationType.Second.Equals("any", StringComparison.OrdinalIgnoreCase) || - allowedLocationType.Second.Equals(to.Type.Identifier, StringComparison.OrdinalIgnoreCase)) + if (allowedConnectionType.Second.Equals("any", StringComparison.OrdinalIgnoreCase) || + allowedConnectionType.Second.Equals(to.Type.Identifier, StringComparison.OrdinalIgnoreCase)) { return true; } } } + if (Type == MissionType.Beacon) + { + var connection = from.Connections.Find(c => c.Locations.Contains(from) && c.Locations.Contains(to)); + if (connection?.LevelData == null || !connection.LevelData.HasBeaconStation || connection.LevelData.IsBeaconActive) { return false; } + } + return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs index 178611d32..a2ff73335 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs @@ -16,6 +16,7 @@ namespace Barotrauma private readonly float maxSonarMarkerDistance = 10000.0f; + private readonly Level.PositionType spawnPosType; public override IEnumerable SonarPositions { @@ -52,6 +53,13 @@ namespace Barotrauma maxSonarMarkerDistance = prefab.ConfigElement.GetAttributeFloat("maxsonarmarkerdistance", 10000.0f); + var spawnPosTypeStr = prefab.ConfigElement.GetAttributeString("spawntype", ""); + if (string.IsNullOrWhiteSpace(spawnPosTypeStr) || + !Enum.TryParse(spawnPosTypeStr, true, out spawnPosType)) + { + spawnPosType = Level.PositionType.MainPath | Level.PositionType.SidePath; + } + foreach (var monsterElement in prefab.ConfigElement.GetChildElements("monster")) { speciesName = monsterElement.GetAttributeString("character", string.Empty); @@ -81,22 +89,32 @@ namespace Barotrauma TextManager.Get("character." + characterParams.SpeciesName)); } } - - public override void Start(Level level) + + protected override void StartMissionSpecific(Level level) { if (monsters.Count > 0) { +#if DEBUG throw new Exception($"monsters.Count > 0 ({monsters.Count})"); +#else + DebugConsole.AddWarning("Monster list was not empty at the start of a monster mission. The mission instance may not have been ended correctly on previous rounds."); + monsters.Clear(); +#endif } if (tempSonarPositions.Count > 0) { +#if DEBUG throw new Exception($"tempSonarPositions.Count > 0 ({tempSonarPositions.Count})"); +#else + DebugConsole.AddWarning("Sonar position list was not empty at the start of a monster mission. The mission instance may not have been ended correctly on previous rounds."); + tempSonarPositions.Clear(); +#endif } if (!IsClient) { - Level.Loaded.TryGetInterestingPosition(true, Level.PositionType.MainPath | Level.PositionType.SidePath, Level.Loaded.Size.X * 0.3f, out Vector2 spawnPos); + Level.Loaded.TryGetInterestingPosition(true, spawnPosType, Level.Loaded.Size.X * 0.3f, out Vector2 spawnPos); foreach (var monster in monsterPrefabs) { int amount = Rand.Range(monster.Item2.X, monster.Item2.Y + 1); @@ -115,7 +133,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) { @@ -203,9 +221,17 @@ namespace Barotrauma tempSonarPositions.Clear(); monsters.Clear(); if (State < 1) { return; } - + + if (Prefab.LocationTypeChangeOnCompleted != null) + { + ChangeLocationType(Prefab.LocationTypeChangeOnCompleted); + } GiveReward(); completed = true; + if (level?.LevelData != null && Prefab.Tags.Any(t => t.Equals("huntinggrounds", StringComparison.OrdinalIgnoreCase))) + { + level.LevelData.HasHuntingGrounds = false; + } } public bool IsEliminated(Character enemy) => diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs index 9bcfdbc0b..2b4c2d1a9 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; @@ -33,7 +35,14 @@ namespace Barotrauma { get { - yield return nestPosition; + if (State > 0) + { + Enumerable.Empty(); + } + else + { + yield return nestPosition; + } } } @@ -46,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", ""); @@ -55,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); @@ -79,8 +90,18 @@ namespace Barotrauma } - public override void Start(Level level) + protected override void StartMissionSpecific(Level level) { + if (items.Any()) + { +#if DEBUG + throw new Exception($"items.Count > 0 ({items.Count})"); +#else + DebugConsole.AddWarning("Item list was not empty at the start of a nest mission. The mission instance may not have been ended correctly on previous rounds."); + items.Clear(); +#endif + } + if (!IsClient) { //ruin/cave/wreck items are allowed to spawn close to the sub @@ -90,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()) { @@ -171,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) @@ -258,9 +303,17 @@ namespace Barotrauma public override void End() { - if (!AllItemsDestroyedOrRetrieved()) + if (AllItemsDestroyedOrRetrieved()) { - return; + GiveReward(); + completed = true; + if (completed) + { + if (Prefab.LocationTypeChangeOnCompleted != null) + { + ChangeLocationType(Prefab.LocationTypeChangeOnCompleted); + } + } } foreach (Item item in items) { @@ -270,8 +323,6 @@ namespace Barotrauma } } items.Clear(); - GiveReward(); - completed = true; failed = !completed && state > 0; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs index defadaf0a..923afb664 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; @@ -101,7 +102,7 @@ namespace Barotrauma } } - public override void Start(Level level) + protected override void StartMissionSpecific(Level level) { #if SERVER originalInventoryID = Entity.NullEntityID; @@ -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 } } @@ -248,6 +253,11 @@ namespace Barotrauma return; } + if (Prefab.LocationTypeChangeOnCompleted != null) + { + ChangeLocationType(Prefab.LocationTypeChangeOnCompleted); + } + item?.Remove(); item = null; GiveReward(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs index f7f9c870c..9e33ac3c1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs @@ -16,8 +16,6 @@ namespace Barotrauma private readonly float scatter; private readonly float offset; - private readonly bool spawnDeep; - private Vector2? spawnPos; private readonly bool disallowed; @@ -73,14 +71,18 @@ namespace Barotrauma maxAmount = Math.Max(prefab.ConfigElement.GetAttributeInt("maxamount", 1), minAmount); var spawnPosTypeStr = prefab.ConfigElement.GetAttributeString("spawntype", ""); - if (string.IsNullOrWhiteSpace(spawnPosTypeStr) || !Enum.TryParse(spawnPosTypeStr, true, out spawnPosType)) { spawnPosType = Level.PositionType.MainPath; } - spawnDeep = prefab.ConfigElement.GetAttributeBool("spawndeep", false); + //backwards compatibility + if (prefab.ConfigElement.GetAttributeBool("spawndeep", false)) + { + spawnPosType = Level.PositionType.Abyss; + } + offset = prefab.ConfigElement.GetAttributeFloat("offset", 0); scatter = Math.Clamp(prefab.ConfigElement.GetAttributeFloat("scatter", 1000), 0, 3000); @@ -138,6 +140,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) @@ -154,19 +161,10 @@ namespace Barotrauma { continue; } - if (Level.Loaded.ExtraWalls.Any(w => w.Cells.Any(c => c.IsPointInside(position.Position.ToVector2())))) + if (Level.Loaded.ExtraWalls.Any(w => w.IsPointInside(position.Position.ToVector2()))) { removals.Add(position); } - if (spawnDeep) - { - for (int i = 0; i < availablePositions.Count; i++) - { - var pos = availablePositions[i].Position; - pos = new Point(pos.X, pos.Y - Level.Loaded.Size.Y); - availablePositions[i] = new Level.InterestingPosition(pos, availablePositions[i].PositionType); - } - } if (position.Position.Y < Level.Loaded.GetBottomPosition(position.Position.X).Y) { removals.Add(position); @@ -180,33 +178,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 (affectSubImmediately && !isSubOrWreck && spawnPosType != Level.PositionType.Abyss) { 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 +249,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 +263,21 @@ 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++) + { + if (Submarine.MainSubs[i] == null) { continue; } + 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 +346,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; } @@ -342,7 +355,7 @@ namespace Barotrauma if (spawnPending) { //wait until there are no submarines at the spawnpos - if (spawnPosType == Level.PositionType.MainPath) + if (spawnPosType == Level.PositionType.MainPath || spawnPosType == Level.PositionType.SidePath || spawnPosType == Level.PositionType.Abyss) { foreach (Submarine submarine in Submarine.Loaded) { @@ -381,6 +394,19 @@ namespace Barotrauma if (!someoneNearby) { return; } } + + if (spawnPosType == Level.PositionType.Abyss || spawnPosType == Level.PositionType.AbyssCave) + { + foreach (Submarine submarine in Submarine.Loaded) + { + if (submarine.Info.Type != SubmarineType.Player) { continue; } + if (submarine.WorldPosition.Y > Level.Loaded.AbyssStart) + { + return; + } + } + } + spawnPending = false; //+1 because Range returns an integer less than the max value @@ -412,7 +438,16 @@ namespace Barotrauma } } - monsters.Add(Character.Create(speciesName, pos, seed, characterInfo: null, isRemotePlayer: false, hasAi: true, createNetworkEvent: true)); + Character createdCharacter = Character.Create(speciesName, pos, seed, characterInfo: null, isRemotePlayer: false, hasAi: true, createNetworkEvent: true); + if (GameMain.GameSession.IsCurrentLocationRadiated()) + { + AfflictionPrefab radiationPrefab = AfflictionPrefab.RadiationSickness; + Affliction affliction = new Affliction(radiationPrefab, radiationPrefab.MaxStrength); + createdCharacter?.CharacterHealth.ApplyAffliction(null, affliction); + // TODO test multiplayer + createdCharacter?.Kill(CauseOfDeathType.Affliction, affliction, log: false); + } + monsters.Add(createdCharacter); if (monsters.Count == amount) { @@ -421,7 +456,7 @@ namespace Barotrauma //otherwise it'll make the spawned characters act as a swarm SwarmBehavior.CreateSwarm(monsters.Cast()); } - }, Rand.Range(0f, amount / 2)); + }, Rand.Range(0f, amount / 2f)); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs index c97e728c3..50fe57605 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs @@ -13,6 +13,9 @@ namespace Barotrauma private int prevEntityCount; private int prevPlayerCount, prevBotCount; + private readonly string[] requiredDestinationTypes; + public readonly bool RequireBeaconStation; + public int CurrentActionIndex { get; private set; } public List Actions { get; } = new List(); public Dictionary> Targets { get; } = new Dictionary>(); @@ -39,6 +42,9 @@ namespace Barotrauma { DebugConsole.ThrowError($"Scripted event \"{prefab.Identifier}\" has no actions. The event will do nothing."); } + + requiredDestinationTypes = prefab.ConfigElement.GetAttributeStringArray("requireddestinationtypes", null); + RequireBeaconStation = prefab.ConfigElement.GetAttributeBool("requirebeaconstation", false); } public void AddTarget(string tag, Entity target) @@ -199,5 +205,21 @@ namespace Barotrauma currentAction.Update(deltaTime); } } + + public override bool LevelMeetsRequirements() + { + if (requiredDestinationTypes == null) { return true; } + var currLocation = GameMain.GameSession?.Campaign?.Map.CurrentLocation; + if (currLocation?.Connections == null) { return true; } + foreach (LocationConnection c in currLocation.Connections) + { + if (RequireBeaconStation && !c.LevelData.HasBeaconStation) { continue; } + if (requiredDestinationTypes.Any(t => c.OtherLocation(currLocation).Type.Identifier.Equals(t, StringComparison.OrdinalIgnoreCase))) + { + return true; + } + } + return false; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs index 950f1f6a0..040f24a6a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs @@ -49,6 +49,19 @@ namespace Barotrauma.Extensions return count == 0 ? default : source.ElementAt(Rand.Range(0, count, randSync)); } } + public static T GetRandom(this IEnumerable source, Random random) + { + if (source is IList list) + { + int count = list.Count; + return count == 0 ? default : list[random.Next(0, count)]; + } + else + { + int count = source.Count(); + return count == 0 ? default : source.ElementAt(random.Next(0, count)); + } + } public static T RandomElementByWeight(this IEnumerable source, Func weightSelector, Rand.RandSync randSync = Rand.RandSync.Unsynced) { 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 4101a63b7..5453f987a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs @@ -25,11 +25,12 @@ namespace Barotrauma subs.ForEach(s => s.Info.InitialSuppliesSpawned = true); } - foreach (var wreck in Submarine.Loaded) + foreach (var sub in Submarine.Loaded) { - if (wreck.Info.IsWreck) + if (sub.Info.Type == SubmarineType.Wreck || + sub.Info.Type == SubmarineType.BeaconStation) { - Place(wreck.ToEnumerable()); + Place(sub.ToEnumerable()); } } @@ -195,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..cb4ec388a 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,23 +140,47 @@ 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() { - CreateItems(PurchasedItems); + CreateItems(PurchasedItems, Submarine.MainSub); OnPurchasedItemsChanged?.Invoke(); } - public static void CreateItems(List itemsToSpawn) + public static void CreateItems(List itemsToSpawn, Submarine sub) { if (itemsToSpawn.Count == 0) { return; } - WayPoint wp = WayPoint.GetRandom(SpawnType.Cargo, null, Submarine.MainSub); + WayPoint wp = WayPoint.GetRandom(SpawnType.Cargo, null, sub); if (wp == null) { DebugConsole.ThrowError("The submarine must have a waypoint marked as Cargo for bought items to be placed correctly!"); @@ -160,85 +188,73 @@ namespace Barotrauma } Hull cargoRoom = Hull.FindHull(wp.WorldPosition); - if (cargoRoom == null) { DebugConsole.ThrowError("A waypoint marked as Cargo must be placed inside a room!"); return; } -#if CLIENT - new GUIMessageBox("", TextManager.GetWithVariable("CargoSpawnNotification", "[roomname]", cargoRoom.DisplayName, true), new string[0], type: GUIMessageBox.Type.InGame, iconStyle: "StoreShoppingCrateIcon"); -#else - foreach (Client client in GameMain.Server.ConnectedClients) + if (sub == Submarine.MainSub) { - ChatMessage msg = ChatMessage.Create("", $"CargoSpawnNotification~[roomname]=§{cargoRoom.RoomName}", ChatMessageType.ServerMessageBoxInGame, null); - msg.IconStyle = "StoreShoppingCrateIcon"; - GameMain.Server.SendDirectChatMessage(msg, client); - } +#if CLIENT + new GUIMessageBox("", TextManager.GetWithVariable("CargoSpawnNotification", "[roomname]", cargoRoom.DisplayName, true), new string[0], type: GUIMessageBox.Type.InGame, iconStyle: "StoreShoppingCrateIcon"); +#else + foreach (Client client in GameMain.Server.ConnectedClients) + { + 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 +269,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 +292,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..84b86b873 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) @@ -122,14 +145,22 @@ namespace Barotrauma } #if CLIENT AddCharacterToCrewList(character); - AddCurrentOrderIcon(character, character.CurrentOrder, character.CurrentOrderOption); -#endif - var idleObjective = character.AIController?.ObjectiveManager?.GetObjective(); - if (idleObjective != null) + if (character.CurrentOrders != null) { - idleObjective.Behavior = character.Info.Job.Prefab.IdleBehavior; + foreach (var order in character.CurrentOrders) + { + AddCurrentOrderIcon(character, order); + } } - +#endif + if (character.AIController is HumanAIController humanAI) + { + var idleObjective = humanAI.ObjectiveManager.GetObjective(); + if (idleObjective != null) + { + idleObjective.Behavior = character.Info.Job.Prefab.IdleBehavior; + } + } } public void AddCharacterInfo(CharacterInfo characterInfo) @@ -150,7 +181,7 @@ namespace Barotrauma List spawnWaypoints = null; List mainSubWaypoints = WayPoint.SelectCrewSpawnPoints(characterInfos, Submarine.MainSub).ToList(); - if (Level.IsLoadedOutpost) + if (Level.IsLoadedOutpost && Submarine.Loaded.Any(s => s.Info.Type == SubmarineType.Outpost && (s.Info.OutpostGenerationParams?.SpawnCrewInsideOutpost ?? false))) { spawnWaypoints = WayPoint.WayPointList.FindAll(wp => wp.SpawnType == SpawnType.Human && @@ -177,7 +208,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) { @@ -222,7 +253,8 @@ namespace Barotrauma { if (order.Second.HasValue) { order.Second -= deltaTime; } } - ActiveOrders.RemoveAll(o => o.Second.HasValue && o.Second <= 0.0f); + ActiveOrders.RemoveAll(o => (o.Second.HasValue && o.Second <= 0.0f) || + (o.First.TargetEntity != null && o.First.TargetEntity.Removed)); UpdateConversations(deltaTime); UpdateProjectSpecific(deltaTime); @@ -262,8 +294,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 d9e08f63a..af6d43366 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; @@ -32,6 +31,8 @@ namespace Barotrauma protected XElement petsElement; + private List extraMissions = new List(); + public enum TransitionType { None, @@ -75,11 +76,18 @@ namespace Barotrauma get { return map; } } - public override Mission Mission + public override IEnumerable Missions { get { - return Map.CurrentLocation?.SelectedMission; + if (Map.CurrentLocation?.SelectedMission != null) + { + yield return Map.CurrentLocation.SelectedMission; + } + foreach (Mission mission in extraMissions) + { + yield return mission; + } } } @@ -146,7 +154,7 @@ namespace Barotrauma { for (int i = 0; i < wall.SectionCount; i++) { - wall.AddDamage(i, -wall.MaxHealth); + wall.SetDamage(i, 0, createNetworkEvent: false); } } } @@ -186,6 +194,53 @@ namespace Barotrauma /// public event Action BeforeLevelLoading; + + public override void AddExtraMissions(LevelData levelData) + { + extraMissions.Clear(); + + var currentLocation = Map.CurrentLocation; + if (levelData.Type == LevelData.LevelType.Outpost) + { + //if there's an available mission that takes place in the outpost, select it + var availableMissionsInLocation = currentLocation.AvailableMissions.Where(m => m.Locations[0] == currentLocation && m.Locations[1] == currentLocation); + if (availableMissionsInLocation.Any()) + { + currentLocation.SelectedMission = availableMissionsInLocation.FirstOrDefault(); + } + else + { + currentLocation.SelectedMission = null; + } + } + else + { + //if we had selected a mission that takes place in the outpost, deselect it when leaving the outpost + if (currentLocation.SelectedMission?.Locations[0] == currentLocation && + currentLocation.SelectedMission?.Locations[1] == currentLocation) + { + currentLocation.SelectedMission = null; + } + + if (levelData.HasBeaconStation && !levelData.IsBeaconActive) + { + var beaconMissionPrefab = MissionPrefab.List.Find(m => m.Identifier.Equals("beaconnoreward", StringComparison.OrdinalIgnoreCase)); + if (beaconMissionPrefab != null && !Missions.Any(m => m.Prefab.Type == beaconMissionPrefab.Type)) + { + extraMissions.Add(beaconMissionPrefab.Instantiate(Map.SelectedConnection.Locations)); + } + } + if (levelData.HasHuntingGrounds) + { + var huntingGroundsMissionPrefab = MissionPrefab.List.Find(m => m.Identifier.Equals("huntinggroundsnoreward", StringComparison.OrdinalIgnoreCase)); + if (huntingGroundsMissionPrefab != null && !Missions.Any(m => m.Prefab.Type == huntingGroundsMissionPrefab.Type)) + { + extraMissions.Add(huntingGroundsMissionPrefab.Instantiate(Map.SelectedConnection.Locations)); + } + } + } + } + public void LoadNewLevel() { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) @@ -287,7 +342,7 @@ namespace Barotrauma nextLevel = map.StartLocation.LevelData; return TransitionType.End; } - if (Level.Loaded.EndLocation != null && Level.Loaded.EndLocation.Type.HasOutpost && Level.Loaded.EndOutpost != null) + if (Level.Loaded.EndLocation != null && Level.Loaded.EndLocation.HasOutpost() && Level.Loaded.EndOutpost != null) { nextLevel = Level.Loaded.EndLocation.LevelData; return TransitionType.ProgressToNextLocation; @@ -306,13 +361,13 @@ namespace Barotrauma } else if (leavingSub.AtStartPosition) { - if (map.CurrentLocation.Type.HasOutpost && Level.Loaded.StartOutpost != null) + if (map.CurrentLocation.HasOutpost() && Level.Loaded.StartOutpost != null) { nextLevel = map.CurrentLocation.LevelData; return TransitionType.ReturnToPreviousLocation; } - else if (map.SelectedLocation != null && map.SelectedLocation != map.CurrentLocation && !map.CurrentLocation.Type.HasOutpost && - (Level.Loaded.LevelData != map.SelectedConnection.LevelData)) + else if (map.SelectedLocation != null && map.SelectedLocation != map.CurrentLocation && !map.CurrentLocation.HasOutpost() && + map.SelectedConnection != null && Level.Loaded.LevelData != map.SelectedConnection.LevelData) { nextLevel = map.SelectedConnection.LevelData; return TransitionType.LeaveLocation; @@ -481,7 +536,7 @@ namespace Barotrauma { CrewManager.RemoveCharacterInfo(ci); } - ci?.ResetCurrentOrder(); + ci?.ClearCurrentOrders(); } foreach (DockingPort port in DockingPort.List) @@ -511,7 +566,18 @@ namespace Barotrauma } Map.SetLocation(Map.Locations.IndexOf(Map.StartLocation)); Map.SelectLocation(-1); + Map.Radiation.Amount = Map.Radiation.Params.StartingRadiation; + foreach (Location location in Map.Locations) + { + location.TurnsInRadiation = 0; + } EndCampaignProjSpecific(); + + if (CampaignMetadata != null) + { + int loops = CampaignMetadata.GetInt("campaign.endings", 0); + CampaignMetadata.SetValue("campaign.endings", loops + 1); + } } protected virtual void EndCampaignProjSpecific() { } @@ -547,18 +613,14 @@ namespace Barotrauma HumanAIController humanAI = npc.AIController as HumanAIController; if (humanAI == null) { yield return CoroutineStatus.Failure; } - OrderInfo? prevSpeakerOrder = null; - if (humanAI.CurrentOrder != null) - { - prevSpeakerOrder = new OrderInfo(humanAI.CurrentOrder, humanAI.CurrentOrderOption); - } var waitOrder = Order.PrefabList.Find(o => o.Identifier.Equals("wait", StringComparison.OrdinalIgnoreCase)); - humanAI.SetOrder(waitOrder, option: string.Empty, orderGiver: null, speak: false); + humanAI.SetForcedOrder(waitOrder, string.Empty, null); + var waitObjective = humanAI.ObjectiveManager.ForcedOrder; humanAI.FaceTarget(interactor); while (!npc.Removed && !interactor.Removed && Vector2.DistanceSquared(npc.WorldPosition, interactor.WorldPosition) < 300.0f * 300.0f && - humanAI.CurrentOrder == waitOrder && + humanAI.ObjectiveManager.ForcedOrder == waitObjective && humanAI.AllowCampaignInteraction() && !interactor.IsIncapacitated) { @@ -569,17 +631,7 @@ namespace Barotrauma ShowCampaignUI = false; #endif - if (humanAI.CurrentOrder == waitOrder) - { - if (prevSpeakerOrder != null) - { - humanAI.SetOrder(prevSpeakerOrder.Value.Order, prevSpeakerOrder.Value.OrderOption, orderGiver: null, speak: false); - } - else - { - humanAI.SetOrder(null, string.Empty, orderGiver: null, speak: false); - } - } + humanAI.ClearForcedOrder(); yield return CoroutineStatus.Success; } @@ -694,7 +746,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/CoOpMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CoOpMode.cs index 8df39d58e..62a3021ee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CoOpMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CoOpMode.cs @@ -1,10 +1,11 @@ using System; +using System.Collections.Generic; namespace Barotrauma { class CoOpMode : MissionMode { - public CoOpMode(GameModePreset preset, MissionPrefab missionPrefab) : base(preset, ValidateMissionPrefab(missionPrefab, MissionPrefab.CoOpMissionClasses)) { } + public CoOpMode(GameModePreset preset, IEnumerable missionPrefabs) : base(preset, ValidateMissionPrefabs(missionPrefabs, MissionPrefab.CoOpMissionClasses)) { } public CoOpMode(GameModePreset preset, MissionType missionType, string seed) : base(preset, ValidateMissionType(missionType, MissionPrefab.CoOpMissionClasses), seed) { } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameMode.cs index 6faaf1b13..1061e9754 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameMode.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; namespace Barotrauma { @@ -16,9 +17,9 @@ namespace Barotrauma get { return GameMain.GameSession?.CrewManager; } } - public virtual Mission Mission + public virtual IEnumerable Missions { - get { return null; } + get { return Enumerable.Empty(); } } public bool IsSinglePlayer @@ -54,6 +55,8 @@ namespace Barotrauma } public virtual void ShowStartMessage() { } + + public virtual void AddExtraMissions(LevelData levelData) { } public virtual void AddToGUIUpdateList() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MissionMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MissionMode.cs index 39193b8a2..8caf39c4f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MissionMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MissionMode.cs @@ -5,37 +5,43 @@ namespace Barotrauma { abstract partial class MissionMode : GameMode { - private readonly Mission mission; + private readonly List missions = new List(); - public override Mission Mission + public override IEnumerable Missions { get { - return mission; + return missions; } } - public MissionMode(GameModePreset preset, MissionPrefab missionPrefab) + public MissionMode(GameModePreset preset, IEnumerable missionPrefabs) : base(preset) { Location[] locations = { GameMain.GameSession.StartLocation, GameMain.GameSession.EndLocation }; - mission = missionPrefab.Instantiate(locations); + foreach (MissionPrefab missionPrefab in missionPrefabs) + { + missions.Add(missionPrefab.Instantiate(locations)); + } } public MissionMode(GameModePreset preset, MissionType missionType, string seed) : base(preset) { Location[] locations = { GameMain.GameSession.StartLocation, GameMain.GameSession.EndLocation }; - mission = Mission.LoadRandom(locations, seed, false, missionType); + missions.Add(Mission.LoadRandom(locations, seed, false, missionType)); } - protected static MissionPrefab ValidateMissionPrefab(MissionPrefab missionPrefab, Dictionary missionClasses) + protected static IEnumerable ValidateMissionPrefabs(IEnumerable missionPrefabs, Dictionary missionClasses) { - if (ValidateMissionType(missionPrefab.Type, missionClasses) != missionPrefab.Type) + foreach (MissionPrefab missionPrefab in missionPrefabs) { - throw new InvalidOperationException("Cannot start gamemode with mission type " + missionPrefab.Type); + if (ValidateMissionType(missionPrefab.Type, missionClasses) != missionPrefab.Type) + { + throw new InvalidOperationException("Cannot start gamemode with mission type " + missionPrefab.Type); + } } - return missionPrefab; + return missionPrefabs; } protected static MissionType ValidateMissionType(MissionType missionType, Dictionary missionClasses) 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..1ac387dee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/PvPMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/PvPMode.cs @@ -1,11 +1,49 @@ -using System; +using Barotrauma.Networking; +using System.Collections.Generic; namespace Barotrauma { class PvPMode : MissionMode { - public PvPMode(GameModePreset preset, MissionPrefab missionPrefab) : base(preset, ValidateMissionPrefab(missionPrefab, MissionPrefab.PvPMissionClasses)) { } + public PvPMode(GameModePreset preset, IEnumerable missionPrefabs) : base(preset, ValidateMissionPrefabs(missionPrefabs, 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 missions = new List(); + public IEnumerable Missions { get { return missions; } } - public Character.TeamType? WinningTeam; + public CharacterTeamType? WinningTeam; public bool IsRunning { get; private set; } @@ -107,17 +108,17 @@ namespace Barotrauma { this.SavePath = savePath; CrewManager = new CrewManager(gameModePreset != null && gameModePreset.IsSinglePlayer); - GameMode = InstantiateGameMode(gameModePreset, seed, missionType: missionType); + GameMode = InstantiateGameMode(gameModePreset, seed, submarineInfo, missionType: missionType); } /// /// Start a new GameSession with a specific pre-selected mission. /// - public GameSession(SubmarineInfo submarineInfo, GameModePreset gameModePreset, string seed = null, MissionPrefab missionPrefab = null) + public GameSession(SubmarineInfo submarineInfo, GameModePreset gameModePreset, string seed = null, IEnumerable missionPrefabs = null) : this(submarineInfo) { CrewManager = new CrewManager(gameModePreset != null && gameModePreset.IsSinglePlayer); - GameMode = InstantiateGameMode(gameModePreset, seed, missionPrefab: missionPrefab); + GameMode = InstantiateGameMode(gameModePreset, seed, submarineInfo, missionPrefabs: missionPrefabs); } /// @@ -158,28 +159,38 @@ 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, IEnumerable missionPrefabs = null, MissionType missionType = MissionType.None) { if (gameModePreset.GameModeType == typeof(CoOpMode)) { - return missionPrefab != null ? - new CoOpMode(gameModePreset, missionPrefab) : + return missionPrefabs != null ? + new CoOpMode(gameModePreset, missionPrefabs) : new CoOpMode(gameModePreset, missionType, seed ?? ToolBox.RandomSeed(8)); } else if (gameModePreset.GameModeType == typeof(PvPMode)) { - return missionPrefab != null ? - new PvPMode(gameModePreset, missionPrefab) : + return missionPrefabs != null ? + new PvPMode(gameModePreset, missionPrefabs) : new PvPMode(gameModePreset, missionType, seed ?? ToolBox.RandomSeed(8)); } 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)) { @@ -200,7 +211,7 @@ namespace Barotrauma } } - private void CreateDummyLocations() + private void CreateDummyLocations(LocationType? forceLocationType = null) { dummyLocations = new Location[2]; @@ -217,7 +228,7 @@ namespace Barotrauma MTRandom rand = new MTRandom(ToolBox.StringToInt(seed)); for (int i = 0; i < 2; i++) { - dummyLocations[i] = Location.CreateRandom(new Vector2((float)rand.NextDouble() * 10000.0f, (float)rand.NextDouble() * 10000.0f), null, rand, requireOutpost: true); + dummyLocations[i] = Location.CreateRandom(new Vector2((float)rand.NextDouble() * 10000.0f, (float)rand.NextDouble() * 10000.0f), null, rand, requireOutpost: true, forceLocationType: forceLocationType); } } @@ -230,7 +241,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 +263,7 @@ namespace Barotrauma Campaign.Money -= cost; ((CampaignMode)GameMode).PendingSubmarineSwitch = newSubmarine; + return newSubmarine; } public void PurchaseSubmarine(SubmarineInfo newSubmarine) @@ -271,9 +283,38 @@ namespace Barotrauma (OwnedSubmarines != null && OwnedSubmarines.Any(os => os.Name == query.Name)); } + public bool IsCurrentLocationRadiated() + { + if (Map?.CurrentLocation == null || Campaign == null) { return false; } + + bool isRadiated = Map.CurrentLocation.IsRadiated(); + + if (Level.Loaded?.EndLocation is { } endLocation) + { + isRadiated |= endLocation.IsRadiated(); + } + + return isRadiated; + } + public void StartRound(string levelSeed, float? difficulty = null) { - StartRound(LevelData.CreateRandom(levelSeed, difficulty)); + LevelData randomLevel = null; + foreach (Mission mission in Missions.Union(GameMode.Missions)) + { + MissionPrefab missionPrefab = mission.Prefab; + if (missionPrefab != null && + missionPrefab.AllowedLocationTypes.Any() && + !missionPrefab.AllowedConnectionTypes.Any()) + { + LocationType locationType = LocationType.List.FirstOrDefault(lt => missionPrefab.AllowedLocationTypes.Any(m => m.Equals(lt.Identifier, StringComparison.OrdinalIgnoreCase))); + CreateDummyLocations(locationType); + randomLevel = LevelData.CreateRandom(levelSeed, difficulty, requireOutpost: true); + break; + } + } + randomLevel ??= LevelData.CreateRandom(levelSeed, difficulty); + StartRound(randomLevel); } public void StartRound(LevelData levelData, bool mirrorLevel = false, SubmarineInfo startOutpost = null, SubmarineInfo endOutpost = null) @@ -296,17 +337,11 @@ namespace Barotrauma LevelData = levelData; - if (GameMode is CampaignMode campaignMode && GameMode.Mission != null && - LevelData != null && LevelData.Type == LevelData.LevelType.Outpost) - { - campaignMode.Map.CurrentLocation.SelectedMission = null; - } - Submarine.Unload(); 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 +351,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); } @@ -329,11 +364,6 @@ namespace Barotrauma InitializeLevel(level); - GameAnalyticsManager.AddDesignEvent("Submarine:" + Submarine.Info.Name); - GameAnalyticsManager.AddDesignEvent("Level", ToolBox.StringToInt(levelData?.Seed ?? "[NO_LEVEL]")); - GameAnalyticsManager.AddProgressionEvent(GameAnalyticsSDK.Net.EGAProgressionStatus.Start, - GameMode.Preset.Identifier, (Mission == null ? "None" : Mission.GetType().ToString())); - #if CLIENT if (GameMode is CampaignMode) { SteamAchievementManager.OnBiomeDiscovered(levelData.Biome); } @@ -343,7 +373,7 @@ namespace Barotrauma existingRoundSummary.ContinueButton.Visible = true; } - RoundSummary = new RoundSummary(Submarine.Info, GameMode, Mission, StartLocation, EndLocation); + RoundSummary = new RoundSummary(Submarine.Info, GameMode, Missions, StartLocation, EndLocation); if (!(GameMode is TutorialMode) && !(GameMode is TestGameMode)) { @@ -352,7 +382,16 @@ namespace Barotrauma { GUI.AddMessage(levelData.Biome.DisplayName, Color.Lerp(Color.CadetBlue, Color.DarkRed, levelData.Difficulty / 100.0f), 5.0f, playSound: false); GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Destination"), EndLocation.Name), Color.CadetBlue, playSound: false); - GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Mission"), (Mission == null ? TextManager.Get("None") : Mission.Name)), Color.CadetBlue, playSound: false); + if (missions.Count > 1) + { + string joinedMissionNames = string.Join(", ", missions.Select(m => m.Name)); + GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Mission"), joinedMissionNames), Color.CadetBlue, playSound: false); + } + else + { + var mission = missions.FirstOrDefault(); + GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Mission"), mission?.Name ?? TextManager.Get("None")), Color.CadetBlue, playSound: false); + } } else { @@ -389,16 +428,18 @@ namespace Barotrauma Entity.Spawner = new EntitySpawner(); - if (GameMode.Mission != null) { Mission = GameMode.Mission; } - if (GameMode != null) { GameMode.Start(); } - if (GameMode.Mission != null) + missions.Clear(); + GameMode.AddExtraMissions(LevelData); + missions.AddRange(GameMode.Missions); + GameMode.Start(); + foreach (Mission mission in missions) { int prevEntityCount = Entity.GetEntities().Count(); - Mission.Start(Level.Loaded); + mission.Start(Level.Loaded); if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && Entity.GetEntities().Count() != prevEntityCount) { DebugConsole.ThrowError( - "Entity count has changed after starting a mission as a client. " + + $"Entity count has changed after starting a mission ({mission.Prefab.Identifier}) as a client. " + "The clients should not instantiate entities themselves when starting the mission," + " but instead the server should inform the client of the spawned entities using Mission.ServerWriteInitial."); } @@ -438,6 +479,8 @@ namespace Barotrauma } } + GameMain.Config.RecentlyEncounteredCreatures.Clear(); + GameMain.GameScreen.Cam.Position = Character.Controlled?.WorldPosition ?? Submarine.MainSub.WorldPosition; RoundStartTime = Timing.TotalTime; GameMain.ResetFrameTime(); @@ -501,7 +544,7 @@ namespace Barotrauma { Submarine.SetPosition(spawnPos); myPort.Dock(outPostPort); - myPort.Lock(true); + myPort.Lock(true, forcePosition: true, applyEffects: false); } else { @@ -540,21 +583,32 @@ namespace Barotrauma { EventManager?.Update(deltaTime); GameMode?.Update(deltaTime); - Mission?.Update(deltaTime); - + foreach (Mission mission in missions) + { + mission.Update(deltaTime); + } UpdateProjSpecific(deltaTime); } + public Mission GetMission(int index) + { + if (index < 0 || index >= missions.Count) { return null; } + return missions[index]; + } + + public int GetMissionIndex(Mission mission) + { + return missions.IndexOf(mission); + } + partial void UpdateProjSpecific(float deltaTime); public void EndRound(string endMessage, List traitorResults = null, CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None) { - if (Mission != null) { Mission.End(); } - GameAnalyticsManager.AddProgressionEvent( - (Mission == null || Mission.Completed) ? GameAnalyticsSDK.Net.EGAProgressionStatus.Complete : GameAnalyticsSDK.Net.EGAProgressionStatus.Fail, - GameMode.Preset.Identifier, - Mission == null ? "None" : Mission.GetType().ToString()); - + foreach (Mission mission in missions) + { + mission.End(); + } #if CLIENT if (GUI.PauseMenuOpen) { @@ -573,14 +627,14 @@ 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); GameMode?.End(transitionType); EventManager?.EndRound(); StatusEffect.StopAll(); - Mission = null; + missions.Clear(); IsRunning = false; } 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 09fab09a7..fcac2eba3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/InputType.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/InputType.cs @@ -17,11 +17,10 @@ namespace Barotrauma Deselect, Shoot, Command, - ToggleInventory -#if DEBUG - , + ToggleInventory, + TakeOneFromInventorySlot, + TakeHalfFromInventorySlot, NextFireMode, PreviousFireMode -#endif } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs index 8bd1a568c..7647cf0b9 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,25 +95,44 @@ 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; } public override bool CanBePut(Item item, int i) { - return base.CanBePut(item, i) && item.AllowedSlots.Contains(SlotTypes[i]); + return + base.CanBePut(item, i) && item.AllowedSlots.Contains(SlotTypes[i]) && + (SlotTypes[i] == InvSlotType.Any || slots[i].ItemCount < 1); + } + + public override bool CanBePut(ItemPrefab itemPrefab, int i) + { + return + base.CanBePut(itemPrefab, i) && + (SlotTypes[i] == InvSlotType.Any || slots[i].ItemCount < 1); } public bool CanBeAutoMovedToCorrectSlots(Item item) @@ -118,9 +141,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 +153,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 +171,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 +197,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 +231,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 +253,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 +261,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 +277,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 +331,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 +348,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..ee2a7e91a 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; } @@ -29,15 +38,17 @@ namespace Barotrauma.Items.Components private Fixture outsideBlocker; private Body doorBody; + private float dockingCooldown; + private bool docked; private bool obstructedWayPointsDisabled; private float forceLockTimer; //if the submarine isn't in the correct position to lock within this time after docking has been activated, //force the sub to the correct position - const float ForceLockDelay = 1.0f; + 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 +74,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 +104,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 /// @@ -129,20 +149,12 @@ namespace Barotrauma.Items.Components DockingDir = GetDir(DockingTarget); DockingTarget.DockingDir = -DockingDir; } - if (joint != null) - { - CreateJoint(joint is WeldJoint); - LinkHullsToGaps(); - } - else if (DockingTarget.joint != null) - { - if (!GameMain.World.BodyList.Contains(DockingTarget.joint.BodyA) || - !GameMain.World.BodyList.Contains(DockingTarget.joint.BodyB)) - { - DockingTarget.CreateJoint(DockingTarget.joint is WeldJoint); - } - DockingTarget.LinkHullsToGaps(); - } + + //undock and redock to recreate the hulls, gaps and physics bodies + var prevDockingTarget = DockingTarget; + Undock(applyEffects: false); + Dock(prevDockingTarget); + Lock(true, applyEffects: false); } } @@ -169,15 +181,15 @@ namespace Barotrauma.Items.Components private void AttemptDock() { var adjacentPort = FindAdjacentPort(); - - if (adjacentPort != null) Dock(adjacentPort); + if (adjacentPort != null) { Dock(adjacentPort); } } public void Dock(DockingPort target) { - if (item.Submarine.DockedTo.Contains(target.item.Submarine)) return; + if (item.Submarine.DockedTo.Contains(target.item.Submarine)) { return; } forceLockTimer = 0.0f; + dockingCooldown = 0.1f; if (DockingTarget != null) { @@ -219,7 +231,6 @@ namespace Barotrauma.Items.Components #if SERVER if (GameMain.Server != null && (!item.Submarine?.Loading ?? true)) { - originalDockingTargetID = DockingTarget.item.ID; item.CreateServerEvent(this); } #endif @@ -229,7 +240,7 @@ namespace Barotrauma.Items.Components } - public void Lock(bool isNetworkMessage, bool forcePosition = false) + public void Lock(bool isNetworkMessage, bool forcePosition = false, bool applyEffects = true) { #if CLIENT if (GameMain.Client != null && !isNetworkMessage) { return; } @@ -246,18 +257,24 @@ namespace Barotrauma.Items.Components DockingDir = GetDir(DockingTarget); DockingTarget.DockingDir = -DockingDir; - ApplyStatusEffects(ActionType.OnUse, 1.0f); - - Vector2 jointDiff = joint.WorldAnchorB - joint.WorldAnchorA; - if (item.Submarine.PhysicsBody.Mass < DockingTarget.item.Submarine.PhysicsBody.Mass || - DockingTarget.item.Submarine.Info.IsOutpost) + if (applyEffects) { - item.Submarine.SubBody.SetPosition(item.Submarine.SubBody.Position + ConvertUnits.ToDisplayUnits(jointDiff)); + ApplyStatusEffects(ActionType.OnUse, 1.0f); } - else if (DockingTarget.item.Submarine.PhysicsBody.Mass < item.Submarine.PhysicsBody.Mass || - item.Submarine.Info.IsOutpost) + + if (forcePosition) { - DockingTarget.item.Submarine.SubBody.SetPosition(DockingTarget.item.Submarine.SubBody.Position - ConvertUnits.ToDisplayUnits(jointDiff)); + Vector2 jointDiff = joint.WorldAnchorB - joint.WorldAnchorA; + if (item.Submarine.PhysicsBody.Mass < DockingTarget.item.Submarine.PhysicsBody.Mass || + DockingTarget.item.Submarine.Info.IsOutpost) + { + item.Submarine.SubBody.SetPosition(item.Submarine.SubBody.Position + ConvertUnits.ToDisplayUnits(jointDiff)); + } + else if (DockingTarget.item.Submarine.PhysicsBody.Mass < item.Submarine.PhysicsBody.Mass || + item.Submarine.Info.IsOutpost) + { + DockingTarget.item.Submarine.SubBody.SetPosition(DockingTarget.item.Submarine.SubBody.Position - ConvertUnits.ToDisplayUnits(jointDiff)); + } } ConnectWireBetweenPorts(); @@ -266,7 +283,6 @@ namespace Barotrauma.Items.Components #if SERVER if (GameMain.Server != null && (!item.Submarine?.Loading ?? true)) { - originalDockingTargetID = DockingTarget.item.ID; item.CreateServerEvent(this); } #else @@ -311,10 +327,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 +362,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 +414,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 +429,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 +532,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 +630,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; @@ -801,13 +844,17 @@ namespace Barotrauma.Items.Components } } - public void Undock() + public void Undock(bool applyEffects = true) { - if (DockingTarget == null || !docked) return; + if (DockingTarget == null || !docked) { return; } forceLockTimer = 0.0f; + dockingCooldown = 0.1f; - ApplyStatusEffects(ActionType.OnSecondaryUse, 1.0f); + if (applyEffects) + { + ApplyStatusEffects(ActionType.OnSecondaryUse, 1.0f); + } DockingTarget.item.Submarine.ConnectedDockingPorts.Remove(item.Submarine); item.Submarine.ConnectedDockingPorts.Remove(DockingTarget.item.Submarine); @@ -879,7 +926,6 @@ namespace Barotrauma.Items.Components #if SERVER if (GameMain.Server != null && (!item.Submarine?.Loading ?? true)) { - originalDockingTargetID = Entity.NullEntityID; item.CreateServerEvent(this); } #endif @@ -889,6 +935,7 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { + dockingCooldown -= deltaTime; if (DockingTarget == null) { dockingState = MathHelper.Lerp(dockingState, 0.0f, deltaTime * 10.0f); @@ -896,7 +943,6 @@ namespace Barotrauma.Items.Components item.SendSignal(0, "0", "state_out", null); item.SendSignal(0, (FindAdjacentPort() != null) ? "1" : "0", "proximity_sensor", null); - } else { @@ -908,7 +954,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 +963,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 +997,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 +1027,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 +1036,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; } } @@ -1055,6 +1109,8 @@ namespace Barotrauma.Items.Components { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } + if (dockingCooldown > 0.0f) { return; } + bool wasDocked = docked; DockingPort prevDockingTarget = DockingTarget; 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/ElectricalDischarger.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs index c91d04396..721113743 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs @@ -62,7 +62,7 @@ namespace Barotrauma.Items.Components set; } - [Serialize(0.25f, true, description: "The duration of an individual discharge (in seconds)."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] + [Serialize(0.25f, true, description: "The duration of an individual discharge (in seconds)."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 60.0f, ValueStep = 0.1f, DecimalCount = 2)] public float Duration { get; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs index 4d2175f14..27c566b83 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs @@ -2,15 +2,14 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Numerics; using System.Xml.Linq; using Barotrauma.Extensions; -using Barotrauma.MapCreatures.Behavior; using Barotrauma.Networking; using FarseerPhysics; using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using Vector2 = Microsoft.Xna.Framework.Vector2; +using Vector4 = Microsoft.Xna.Framework.Vector4; namespace Barotrauma.Items.Components { @@ -286,8 +285,19 @@ namespace Barotrauma.Items.Components } } - int value = pool[Growable.RandomInt(0, possible, random)]; + int value; + if (Parent == null) + { + value = pool[Growable.RandomInt(0, possible, random)]; + } + else + { + var (x, y, z, w) = Parent.GrowthWeights; + float[] weights = { x, y, z, w }; + value = pool.RandomElementByWeight(i => weights[i]); + } + return (TileSide) (1 << value); } @@ -382,6 +392,12 @@ namespace Barotrauma.Items.Components [Serialize("0.26,0.27,0.29,1.0", true, "Tint of a dead plant.")] public Color DeadTint { get; set; } + [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; @@ -405,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(); @@ -476,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) @@ -617,6 +638,8 @@ namespace Barotrauma.Items.Components { base.Update(deltaTime, cam); + UpdateFires(deltaTime); + #if CLIENT foreach (VineTile vine in Vines) { @@ -627,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; } @@ -677,7 +723,23 @@ namespace Barotrauma.Items.Components TileSide side = oldVines.GetRandomFreeSide(random); - if (side == TileSide.None) { continue; } + if (side == TileSide.None) + { + oldVines.FailedGrowthAttempts++; + continue; + } + + if (GrowthWeights != Vector4.One) + { + var (x, y, z, w) = GrowthWeights; + float[] weights = { x, y, z, w }; + int index = (int) Math.Log2((int) side); + if (MathUtils.NearlyEqual(weights[index], 0f)) + { + oldVines.FailedGrowthAttempts++; + continue; + } + } Vector2 pos = oldVines.AdjacentPositions[side]; Rectangle rect = VineTile.CreatePlantRect(pos); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index 1837d6e8d..0ee64e880 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,13 +29,22 @@ namespace Barotrauma.Items.Components private float swingState; + private Character prevEquipper; + private bool attachable, attached, attachedByDefault; + private Voronoi2.VoronoiCell attachTargetCell; private readonly PhysicsBody body; public PhysicsBody Pusher { get; private set; } + [Serialize(true, true, description: "Is the item currently able to push characters around? True by default. Only valid if blocksplayers is set to true.")] + public bool CanPush + { + get; + set; + } //the angle in which the Character holds the item protected float holdAngle; @@ -205,6 +214,7 @@ namespace Barotrauma.Items.Components if (other.Body.UserData is Character character) { if (!IsActive) { return false; } + if (!CanPush) { return false; } return character != picker; } else @@ -255,6 +265,7 @@ namespace Barotrauma.Items.Components if (Pusher != null) { Pusher.Enabled = false; } if (item.body != null) { item.body.Enabled = true; } IsActive = false; + attachTargetCell = null; if (picker == null || picker.Removed) { @@ -299,11 +310,9 @@ namespace Barotrauma.Items.Components { item.SetTransform(picker.SimPosition, 0.0f); } - } - + } } - picker.DeselectItem(item); picker.Inventory.RemoveItem(item); picker = null; } @@ -338,34 +347,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; @@ -383,9 +391,9 @@ namespace Barotrauma.Items.Components //can be attached anywhere inside hulls if (item.CurrentHull != null && Submarine.RectContains(item.CurrentHull.WorldRect, attachPos)) { return true; } - return Structure.GetAttachTarget(attachPos) != null; + return Structure.GetAttachTarget(attachPos) != null || GetAttachTargetCell(100.0f) != null; } - + public bool CanBeDeattached() { if (!attachable || !attached) { return true; } @@ -399,14 +407,14 @@ namespace Barotrauma.Items.Components //if the item has a connection panel and rewiring is disabled, don't allow deattaching var connectionPanel = item.GetComponent(); - if (connectionPanel != null && (connectionPanel.Locked || !(GameMain.NetworkMember?.ServerSettings?.AllowRewiring ?? true))) + if (connectionPanel != null && !connectionPanel.AlwaysAllowRewiring && (connectionPanel.Locked || !(GameMain.NetworkMember?.ServerSettings?.AllowRewiring ?? true))) { return false; } if (item.CurrentHull == null) { - return Structure.GetAttachTarget(item.WorldPosition) != null; + return attachTargetCell != null && Structure.GetAttachTarget(item.WorldPosition) != null; } else { @@ -464,7 +472,7 @@ namespace Barotrauma.Items.Components public void AttachToWall() { - if (!attachable) return; + if (!attachable) { return; } //outside hulls/subs -> we need to check if the item is being attached on a structure outside the sub if (item.CurrentHull == null && item.Submarine == null) @@ -479,15 +487,19 @@ namespace Barotrauma.Items.Components } item.Submarine = attachTarget.Submarine; } + else + { + attachTargetCell = GetAttachTargetCell(150.0f); + if (attachTargetCell != null) { IsActive = true; } + } } - 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); } } @@ -507,6 +519,7 @@ namespace Barotrauma.Items.Components if (!attachable) return; Attached = false; + attachTargetCell = null; //make the item pickable with the default pick key and with no specific tools/items when it's deattached requiredItems.Clear(); @@ -568,9 +581,48 @@ namespace Barotrauma.Items.Components Vector2 userPos = useWorldCoordinates ? user.WorldPosition : user.Position; - return new Vector2( - MathUtils.RoundTowardsClosest(userPos.X + mouseDiff.X, Submarine.GridSize.X), - MathUtils.RoundTowardsClosest(userPos.Y + mouseDiff.Y, Submarine.GridSize.Y)); + Vector2 attachPos = userPos + mouseDiff; + + if (user.Submarine == null && Level.Loaded != null) + { + bool edgeFound = false; + foreach (var cell in Level.Loaded.GetCells(attachPos)) + { + if (cell.CellType != Voronoi2.CellType.Solid) { continue; } + foreach (var edge in cell.Edges) + { + if (!edge.IsSolid) { continue; } + if (MathUtils.GetLineIntersection(edge.Point1, edge.Point2, user.WorldPosition, attachPos, out Vector2 intersection)) + { + attachPos = intersection; + edgeFound = true; + break; + } + } + if (edgeFound) { break; } + } + } + + return + new Vector2( + MathUtils.RoundTowardsClosest(attachPos.X, Submarine.GridSize.X), + MathUtils.RoundTowardsClosest(attachPos.Y, Submarine.GridSize.Y)); + } + + private Voronoi2.VoronoiCell GetAttachTargetCell(float maxDist) + { + if (Level.Loaded == null) { return null; } + foreach (var cell in Level.Loaded.GetCells(item.WorldPosition, searchDepth: 1)) + { + if (cell.CellType != Voronoi2.CellType.Solid) { continue; } + Vector2 diff = cell.Center - item.WorldPosition; + if (diff.LengthSquared() > 0.0001f) { diff = Vector2.Normalize(diff); } + if (cell.IsPointInside(item.WorldPosition + diff * maxDist)) + { + return cell; + } + } + return null; } public override void UpdateBroken(float deltaTime, Camera cam) @@ -580,14 +632,28 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { + if (attachTargetCell != null) + { + if (attachTargetCell.CellType != Voronoi2.CellType.Solid) + { + Drop(dropConnectedWires: true, dropper: null); + } + return; + } + if (item.body == null || !item.body.Enabled) { return; } if (picker == null || !picker.HasEquippedItem(item)) { if (Pusher != null) { Pusher.Enabled = false; } - IsActive = false; + if (attachTargetCell == null) { IsActive = false; } return; } + if (picker == Character.Controlled && picker.IsKeyDown(InputType.Aim) && CanBeAttached(picker)) + { + Drawable = true; + } + Vector2 swing = Vector2.Zero; if (swingAmount != Vector2.Zero && !picker.IsUnconscious && picker.Stun <= 0.0f) { @@ -612,7 +678,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 5bde257d5..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; } @@ -77,7 +84,7 @@ namespace Barotrauma.Items.Components } else { - if (Vector2.DistanceSquared(item.SimPosition, trigger.SimPosition) > 0.01f) + if (trigger != null && Vector2.DistanceSquared(item.SimPosition, trigger.SimPosition) > 0.01f) { trigger.SetTransform(item.SimPosition, 0.0f); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index 6c4d2e8e0..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,13 +147,15 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { if (!item.body.Enabled) { impactQueue.Clear(); return; } - if (!picker.HasSelectedItem(item)) { impactQueue.Clear(); IsActive = false; } + if (picker == null && !picker.HeldItems.Contains(item)) { impactQueue.Clear(); IsActive = false; } while (impactQueue.Count > 0) { var impact = impactQueue.Dequeue(); HandleImpact(impact.Body); } + //in case handling the impact does something to the picker + if (picker == null) { return; } reloadTimer -= deltaTime; if (reloadTimer < 0) { reloadTimer = 0; } @@ -242,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 f4dca73d1..94422a9c9 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; } @@ -109,10 +116,11 @@ namespace Barotrauma.Items.Components { get { + if (item.body == null) { return BarrelPos; } Matrix bodyTransform = Matrix.CreateRotationZ(item.body.Rotation + MathHelper.ToRadians(BarrelRotation)); Vector2 flippedPos = BarrelPos; if (item.body.Dir < 0.0f) { flippedPos.X = -flippedPos.X; } - return (Vector2.Transform(flippedPos, bodyTransform)); + return Vector2.Transform(flippedPos, bodyTransform); } } @@ -228,11 +236,15 @@ namespace Barotrauma.Items.Components } float spread = MathHelper.ToRadians(MathHelper.Lerp(UnskilledSpread, Spread, degreeOfSuccess)); - float angle = item.body.Rotation + MathHelper.ToRadians(BarrelRotation) + spread * Rand.Range(-0.5f, 0.5f); - Vector2 rayEnd = rayStartWorld + - ConvertUnits.ToSimUnits(new Vector2( - (float)Math.Cos(angle), - (float)Math.Sin(angle)) * Range * item.body.Dir); + + float angle = MathHelper.ToRadians(BarrelRotation) + spread * Rand.Range(-0.5f, 0.5f); + float dir = 1; + if (item.body != null) + { + angle += item.body.Rotation; + dir = item.body.Dir; + } + Vector2 rayEnd = rayStartWorld + ConvertUnits.ToSimUnits(new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle)) * Range * dir); ignoredBodies.Clear(); if (character != null) @@ -315,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; @@ -356,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; @@ -371,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; } @@ -393,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; } @@ -438,7 +461,7 @@ namespace Barotrauma.Items.Components } } - if (WaterAmount > 0.0f && item.CurrentHull?.Submarine != null) + if (WaterAmount > 0.0f && item.Submarine != null) { Vector2 pos = ConvertUnits.ToDisplayUnits(rayStart + item.Submarine.SimPosition); @@ -466,7 +489,7 @@ namespace Barotrauma.Items.Components #if CLIENT float barOffset = 10f * GUI.Scale; Vector2 offset = planter.PlantSlots.ContainsKey(i) ? planter.PlantSlots[i].Offset : Vector2.Zero; - user.UpdateHUDProgressBar(planter, planter.Item.DrawPosition + new Vector2(barOffset, 0) + offset, seed.Health / seed.MaxHealth, GUI.Style.Blue, GUI.Style.Blue, "progressbar.watering"); + user?.UpdateHUDProgressBar(planter, planter.Item.DrawPosition + new Vector2(barOffset, 0) + offset, seed.Health / seed.MaxHealth, GUI.Style.Blue, GUI.Style.Blue, "progressbar.watering"); #endif } } @@ -490,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; } @@ -519,10 +540,9 @@ namespace Barotrauma.Items.Components } return true; } - else if (targetBody.UserData is Voronoi2.VoronoiCell cell) + 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); } @@ -574,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 && @@ -589,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; } @@ -615,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) @@ -630,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; @@ -638,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) { @@ -729,51 +746,57 @@ 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); - if (leakFixed && leak.FlowTargetHull?.DisplayName != null) + if (leakFixed && leak.FlowTargetHull?.DisplayName != null && character.IsOnPlayerTeam) { if (!leak.FlowTargetHull.ConnectedGaps.Any(g => !g.IsRoomToRoom && g.Open > 0.0f)) { @@ -786,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) @@ -816,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 29503cd75..56b99929b 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; @@ -394,7 +400,10 @@ namespace Barotrauma.Items.Components } //called when isActive is true and condition > 0.0f - public virtual void Update(float deltaTime, Camera cam) { } + public virtual void Update(float deltaTime, Camera cam) + { + ApplyStatusEffects(ActionType.OnActive, deltaTime); + } //called when isActive is true and condition == 0.0f public virtual void UpdateBroken(float deltaTime, Camera cam) @@ -450,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); } @@ -481,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); } @@ -651,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; @@ -669,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; @@ -676,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; } } } @@ -686,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; } } } } @@ -942,33 +952,13 @@ namespace Barotrauma.Items.Components #region AI related protected const float AIUpdateInterval = 0.2f; protected float aiUpdateTimer; - private int itemIndex; - private Character previousUser; - protected bool FindSuitableContainer(Character character, Func priority, out Item suitableContainer) - { - suitableContainer = null; - if (character.AIController is HumanAIController aiController) - { - if (previousUser != character) - { - previousUser = character; - itemIndex = 0; - } - if (character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: aiController.IgnoredItems, customPriorityFunction: priority)) - { - suitableContainer = targetContainer; - return true; - } - } - return false; - } - protected AIObjectiveContainItem AIContainItems(ItemContainer container, Character character, AIObjective objective, int itemCount, bool equip, bool removeEmpty, bool spawnItemIfNotFound = false) where T : ItemComponent + protected AIObjectiveContainItem AIContainItems(ItemContainer container, Character character, AIObjective currentObjective, int itemCount, bool equip, bool removeEmpty, bool spawnItemIfNotFound = false, bool dropItemOnDeselected = false) where T : ItemComponent { AIObjectiveContainItem containObjective = null; if (character.AIController is HumanAIController aiController) { - containObjective = new AIObjectiveContainItem(character, container.GetContainableItemIdentifiers.ToArray(), container, objective.objectiveManager, spawnItemIfNotFound: spawnItemIfNotFound) + containObjective = new AIObjectiveContainItem(character, container.GetContainableItemIdentifiers.ToArray(), container, currentObjective.objectiveManager, spawnItemIfNotFound: spawnItemIfNotFound) { targetItemCount = itemCount, Equip = equip, @@ -986,91 +976,24 @@ namespace Barotrauma.Items.Components return 1.0f; } }; - containObjective.Abandoned += () => + containObjective.Abandoned += () => aiController.IgnoredItems.Add(container.Item); + if (dropItemOnDeselected) { - aiController.IgnoredItems.Add(container.Item); - }; - objective.AddSubObjective(containObjective); + currentObjective.Deselected += () => + { + if (containObjective == null) { return; } + if (containObjective.IsCompleted) { return; } + Item item = containObjective.ItemToContain; + if (item != null && character.CanInteractWith(item, checkLinked: false)) + { + item.Drop(character); + } + }; + } + currentObjective.AddSubObjective(containObjective); } return containObjective; } - - /// - /// Returns true when done seeking the suitable container. - /// - protected bool AIDecontainEmptyItems(Character character, AIObjective objective, bool equip, ItemContainer sourceContainer = null) - { - 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; - foreach (Item containedItem in containedItems) - { - if (containedItem != null && containedItem.Condition <= 0.0f) - { - if (FindSuitableContainer(character, - i => - { - if (i.IsThisOrAnyContainerIgnoredByAI()) { return 0; } - var container = i.GetComponent(); - if (container == null) { return 0; } - if (container.Inventory.IsFull()) { 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; - } - else - { - if (containedItem.Prefab.IsContainerPreferred(container, out bool isPreferencesDefined, out bool isSecondary)) - { - return isPreferencesDefined ? isSecondary ? 2 : 3 : 1; - } - else - { - return isPreferencesDefined ? 0 : 1; - } - } - } - else - { - return 0; - } - }, out Item targetContainer)) - { - var decontainObjective = new AIObjectiveDecontainItem(character, containedItem, objective.objectiveManager, sourceC, targetContainer?.GetComponent()) - { - Equip = equip - }; - decontainObjective.Abandoned += () => - { - itemIndex = 0; - if (targetContainer != null) - { - aiController.IgnoredItems.Add(targetContainer); - } - }; - decontainObjective.Completed += () => - { - if (targetContainer == null) - { - itemIndex = 0; - } - }; - objective.AddSubObjectiveInQueue(decontainObjective); - } - else - { - return false; - } - } - } - } - return true; - } #endregion } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index 8ed2f963b..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.")] @@ -94,6 +116,9 @@ namespace Barotrauma.Items.Components set; } + [Serialize(false, false)] + public bool RemoveContainedItemsOnDeconstruct { get; set; } + public bool ShouldBeContained(string[] identifiersOrTags, out bool isRestrictionsDefined) { isRestrictionsDefined = containableRestrictions.Any(); @@ -138,8 +163,6 @@ namespace Barotrauma.Items.Components } InitProjSpecific(element); - - itemsWithStatusEffects = new List>(); } partial void InitProjSpecific(XElement element); @@ -151,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) @@ -193,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); @@ -237,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; @@ -261,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; @@ -274,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; } @@ -315,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 @@ -359,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; } @@ -377,12 +404,9 @@ namespace Barotrauma.Items.Components if (SpawnWithId.Length > 0) { ItemPrefab prefab = ItemPrefab.Prefabs.Find(m => m.Identifier == SpawnWithId); - if (prefab != null) + if (prefab != null && Inventory != null && Inventory.CanBePut(prefab)) { - if (Inventory != null && Inventory.Items.Any(it => it == null)) - { - Entity.Spawner?.AddToSpawnQueue(prefab, Inventory); - } + 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 05dec7c0d..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) { 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..b34ccba7e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Security.Cryptography; using System.Xml.Linq; namespace Barotrauma.Items.Components @@ -96,6 +97,12 @@ namespace Barotrauma.Items.Components fabricationRecipes.Add(recipe); } } + fabricationRecipes.Sort((r1, r2) => + { + int hash1 = (int)r1.TargetItem.UIntIdentifier; + int hash2 = (int)r2.TargetItem.UIntIdentifier; + return hash1 - hash2; + }); state = FabricatorState.Stopped; @@ -142,7 +149,7 @@ namespace Barotrauma.Items.Components public override bool Pick(Character picker) { - return (picker != null); + return picker != null; } public void RemoveFabricationRecipes(List allowedIdentifiers) @@ -161,10 +168,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 +197,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 +305,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 +339,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 +399,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 +414,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 +428,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 +474,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 e7a199447..f6aa6c589 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs @@ -31,20 +31,6 @@ namespace Barotrauma.Items.Components private float pumpSpeedLockTimer, isActiveLockTimer; - private bool infected; - - [Serialize(false, true, description: "Whether or not the pump is infected with ballast flora spores.")] - public bool Infected - { - get => infected; - set - { - infected = value; - } - } - - public string InfectIdentifier; - [Serialize(0.0f, true, description: "How fast the item is currently pumping water (-100 = full speed out, 100 = full speed in). Intended to be used by StatusEffect conditionals (setting this value in XML has no effect).")] public float FlowPercentage { @@ -116,35 +102,38 @@ 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 currFlow *= MathHelper.Lerp(0.5f, 1.0f, item.Condition / item.MaxCondition); - - if (currFlow < 0 && Infected) - { - InfectBallast(InfectIdentifier); - } - Infected = false; - item.CurrentHull.WaterVolume += currFlow; 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; } + var ballastFloraPrefab = BallastFloraPrefab.Find(identifier); + if (ballastFloraPrefab == null) + { + DebugConsole.ThrowError($"Failed to infect a ballast pump (could not find a ballast flora prefab with the identifier \"{identifier}\").\n" + Environment.StackTrace); + return; + } + Vector2 offset = item.WorldPosition - hull.WorldPosition; - hull.BallastFlora = new BallastFloraBehavior(hull, BallastFloraPrefab.Find(identifier), offset, firstGrowth: true); + hull.BallastFlora = new BallastFloraBehavior(hull, ballastFloraPrefab, offset, firstGrowth: true); #if SERVER hull.BallastFlora.SendNetworkMessage(hull.BallastFlora, BallastFloraBehavior.NetworkHeader.Spawn); @@ -180,7 +169,7 @@ namespace Barotrauma.Items.Components { if (float.TryParse(signal, NumberStyles.Any, CultureInfo.InvariantCulture, out float tempTarget)) { - TargetLevel = MathHelper.Clamp(tempTarget + 50.0f, 0.0f, 100.0f); + TargetLevel = MathUtils.InverseLerp(-100.0f, 100.0f, tempTarget) * 100.0f; pumpSpeedLockTimer = 0.1f; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs index cdc211703..6aac60622 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs @@ -137,7 +137,7 @@ namespace Barotrauma.Items.Components } } - [Serialize(0.2f, true, description: "How fast the condition of the contained fuel rods deteriorates per second."), Editable(0.0f, 1000.0f)] + [Serialize(0.2f, true, description: "How fast the condition of the contained fuel rods deteriorates per second."), Editable(0.0f, 1000.0f, decimals: 3)] public float FuelConsumptionRate { get { return fuelConsumptionRate; } @@ -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; } @@ -399,6 +398,8 @@ namespace Barotrauma.Items.Components //fission rate is clamped to the amount of available fuel float maxFissionRate = Math.Min(prevAvailableFuel, 100.0f); + if (maxFissionRate >= 100.0f) { return false; } + float maxTurbineOutput = 100.0f; //calculate the maximum output if the fission rate is cranked as high as it goes and turbine output is at max @@ -412,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); @@ -530,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; } } @@ -557,58 +557,68 @@ 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)) + if (aiUpdateTimer > 0.0f) { + aiUpdateTimer -= deltaTime; return false; } - } + aiUpdateTimer = AIUpdateInterval; - 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()) + // 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)) { - 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); - 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) + bool outOfFuel = false; + var container = item.GetComponent(); + if (objective.SubObjectives.None()) { - if (item != null && container.ContainableItems.Any(ri => ri.MatchesItem(item))) + int itemCount = item.ContainedItems.Count(i => i != null && container.ContainableItems.Any(ri => ri.MatchesItem(i))) + 1; + var containObjective = AIContainItems(container, character, objective, itemCount, equip: false, removeEmpty: true, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC, dropItemOnDeselected: true); + containObjective.Completed += ReportFuelRodCount; + containObjective.Abandoned += ReportFuelRodCount; + character.Speak(TextManager.Get("DialogReactorFuel"), null, 0.0f, "reactorfuel", 30.0f); + + void ReportFuelRodCount() { - if (!character.Inventory.TryPutItem(item, character, allowedSlots: item.AllowedSlots)) + if (!character.IsOnPlayerTeam) { return; } + int remainingFuelRods = Submarine.MainSub.GetItems(false).Count(i => i.HasTag("reactorfuel") && i.Condition > 1); + if (remainingFuelRods == 0) + { + character.Speak(TextManager.Get("DialogOutOfFuelRods"), null, 0.0f, "outoffuelrods", 30.0f); + outOfFuel = true; + } + else if (remainingFuelRods < 3) + { + character.Speak(TextManager.Get("DialogLowOnFuelRods"), null, 0.0f, "lowonfuelrods", 30.0f); + } + } + } + return outOfFuel; + } + else if (TooMuchFuel()) + { + if (item.OwnInventory?.AllItems != null) + { + var container = item.GetComponent(); + foreach (Item item in item.OwnInventory.AllItemsMod) + { + if (container.ContainableItems.Any(ri => ri.MatchesItem(item))) { item.Drop(character); + break; } - break; } } } @@ -619,13 +629,13 @@ namespace Barotrauma.Items.Components { if (lastUser != null && lastUser != character && lastUser != LastAIUser) { - if (lastUser.SelectedConstruction == item) + if (lastUser.SelectedConstruction == item && character.IsOnPlayerTeam) { character.Speak(TextManager.Get("DialogReactorTaken"), null, 0.0f, "reactortaken", 10.0f); } } } - else if (LastUserWasPlayer) + else if (LastUserWasPlayer && lastUser != null && lastUser.TeamID == character.TeamID) { return true; } @@ -637,48 +647,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() @@ -697,14 +704,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 @@ -714,7 +721,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/Sonar.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs index 9b2451498..439118dc9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs @@ -104,7 +104,8 @@ namespace Barotrauma.Items.Components set; } - [Editable, Serialize(false, false, description: "Does the sonar have mineral scanning mode?")] + [Editable, Serialize(false, false, description: "Does the sonar have mineral scanning mode. " + + "Only available in-game when the Item has no Steering component.")] public bool HasMineralScanner { get; set; } public float Zoom @@ -251,9 +252,10 @@ namespace Barotrauma.Items.Components } foreach (Character c in Character.CharacterList) { - if (c.AnimController.CurrentHull != null || !c.Enabled) continue; - if (DetectSubmarineWalls && c.AnimController.CurrentHull == null && item.CurrentHull != null) continue; - if (Vector2.DistanceSquared(c.WorldPosition, item.WorldPosition) > range * range) continue; + if (c.IsDead || c.Removed || !c.Enabled) { continue; } + if (c.AnimController.CurrentHull != null || c.Params.HideInSonar) { continue; } + if (DetectSubmarineWalls && c.AnimController.CurrentHull == null && item.CurrentHull != null) { continue; } + if (Vector2.DistanceSquared(c.WorldPosition, item.WorldPosition) > range * range) { continue; } string directionName = GetDirectionName(c.WorldPosition - item.WorldPosition); if (!targetGroups.ContainsKey(directionName)) @@ -276,9 +278,12 @@ namespace Barotrauma.Items.Components dialogTag = "DialogSonarTargetLarge"; } - character.Speak(TextManager.GetWithVariables(dialogTag, new string[2] { "[direction]", "[count]" }, - new string[2] { targetGroup.Key.ToString(), targetGroup.Value.Count.ToString() }, - new bool[2] { true, false }), null, 0, "sonartarget" + targetGroup.Value[0].ID, 60); + if (character.IsOnPlayerTeam) + { + character.Speak(TextManager.GetWithVariables(dialogTag, new string[2] { "[direction]", "[count]" }, + new string[2] { targetGroup.Key.ToString(), targetGroup.Value.Count.ToString() }, + new bool[2] { true, false }), null, 0, "sonartarget" + targetGroup.Value[0].ID, 60); + } //prevent the character from reporting other targets in the group for (int i = 1; i < targetGroup.Value.Count; i++) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs index 4dbeb06b4..e8ee7805b 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; } @@ -67,7 +72,10 @@ namespace Barotrauma.Items.Components { if (pathFinder == null) { - pathFinder = new PathFinder(WayPoint.WayPointList, false); + pathFinder = new PathFinder(WayPoint.WayPointList, false) + { + GetNodePenalty = GetNodePenalty + }; } MaintainPos = true; if (posToMaintain == null) @@ -87,7 +95,7 @@ namespace Barotrauma.Items.Components } } - [Editable(0.0f, 1.0f, decimals: 3), + [Editable(0.0f, 1.0f, decimals: 4), Serialize(0.5f, true, description: "How full the ballast tanks should be when the submarine is not being steered upwards/downwards." + " Can be used to compensate if the ballast tanks are too large/small relative to the size of the submarine.")] public float NeutralBallastLevel @@ -299,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) @@ -323,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); } @@ -342,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) @@ -351,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; } @@ -365,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); @@ -417,27 +440,38 @@ 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) { - if (Level.Loaded?.ExtraWalls.Any(w => w.WallDamageOnTouch > 0.0f && w.Cells.Contains(cell)) ?? false) + if (cell.DoesDamage) { 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; } @@ -453,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 ? @@ -480,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; @@ -512,6 +545,15 @@ namespace Barotrauma.Items.Components } } + private float? GetNodePenalty(PathNode node, PathNode nextNode) + { + if (node.Waypoint?.Tunnel == null || controlledSub == null || node.Waypoint.Tunnel.Type == Level.TunnelType.MainPath) { return 0.0f; } + //never navigate from the main path to another type of path + if (node.Waypoint.Tunnel.Type == Level.TunnelType.MainPath && nextNode.Waypoint?.Tunnel?.Type != Level.TunnelType.MainPath) { return null; } + //higher cost for side paths (= autopilot prefers the main path, but can still navigate side paths if it ends up on one) + return 1000.0f; + } + private void UpdatePath() { if (Level.Loaded == null) { return; } @@ -584,7 +626,7 @@ namespace Barotrauma.Items.Components { if (objective.Override) { - if (user != character && user != null && user.SelectedConstruction == item) + if (user != character && user != null && user.SelectedConstruction == item && character.IsOnPlayerTeam) { character.Speak(TextManager.Get("DialogSteeringTaken"), null, 0.0f, "steeringtaken", 10.0f); } @@ -647,6 +689,10 @@ namespace Barotrauma.Items.Components break; } sonar?.AIOperate(deltaTime, character, objective); + if (!MaintainPos && showIceSpireWarning && character.IsOnPlayerTeam) + { + character.Speak(TextManager.Get("dialogicespirespottedsonar"), null, 0.0f, "icespirespottedsonar", 60.0f); + } return false; } @@ -654,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/PowerContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs index 246217863..0ae0bcdbc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs @@ -228,11 +228,13 @@ namespace Barotrauma.Items.Components { rechargeSpeedSlider.BarScroll = RechargeSpeed / Math.Max(maxRechargeSpeed, 1.0f); } -#endif - - character.Speak(TextManager.GetWithVariables("DialogChargeBatteries", new string[2] { "[itemname]", "[rate]" }, - new string[2] { item.Name, ((int)(rechargeSpeed / maxRechargeSpeed * 100.0f)).ToString() }, - new bool[2] { true, false }), null, 1.0f, "chargebattery", 10.0f); +#endif + if (character.IsOnPlayerTeam) + { + character.Speak(TextManager.GetWithVariables("DialogChargeBatteries", new string[2] { "[itemname]", "[rate]" }, + new string[2] { item.Name, ((int)(rechargeSpeed / maxRechargeSpeed * 100.0f)).ToString() }, + new bool[2] { true, false }), null, 1.0f, "chargebattery", 10.0f); + } } } else @@ -249,9 +251,12 @@ namespace Barotrauma.Items.Components rechargeSpeedSlider.BarScroll = RechargeSpeed / Math.Max(maxRechargeSpeed, 1.0f); } #endif - character.Speak(TextManager.GetWithVariables("DialogStopChargingBatteries", new string[2] { "[itemname]", "[rate]" }, - new string[2] { item.Name, ((int)(rechargeSpeed / maxRechargeSpeed * 100.0f)).ToString() }, - new bool[2] { true, false }), null, 1.0f, "chargebattery", 10.0f); + if (character.IsOnPlayerTeam) + { + character.Speak(TextManager.GetWithVariables("DialogStopChargingBatteries", new string[2] { "[itemname]", "[rate]" }, + new string[2] { item.Name, ((int)(rechargeSpeed / maxRechargeSpeed * 100.0f)).ToString() }, + new bool[2] { true, false }), null, 1.0f, "chargebattery", 10.0f); + } } } 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 9205afa79..56fa478d5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -213,12 +213,15 @@ namespace Barotrauma.Items.Components private void Launch(Character user, Vector2 simPosition, float rotation) { - //User = user; Item.body.ResetDynamics(); Item.SetTransform(simPosition, rotation); - Use(); - if (Item.Removed) { return; } + // Set user for hitscan projectiles to work properly. User = user; + // Need to set null for non-characterusable items. + Use(character: null); + // Set user for normal projectiles to work properly. + User = user; + if (Item.Removed) { return; } launchPos = simPosition; //set the rotation of the projectile again because dropping the projectile resets the rotation Item.SetTransform(simPosition, rotation + (Item.body.Dir * LaunchRotationRadians)); @@ -328,21 +331,21 @@ namespace Barotrauma.Items.Components IsActive = true; Vector2 rayStart = simPositon; - Vector2 rayEnd = simPositon + dir * 1000.0f; + Vector2 rayEnd = simPositon + dir * 500.0f; List hits = new List(); - hits.AddRange(DoRayCast(rayStart, rayEnd)); + hits.AddRange(DoRayCast(rayStart, rayEnd, submarine: item.Submarine)); if (item.Submarine != null) { //shooting indoors, do a hitscan outside as well - hits.AddRange(DoRayCast(rayStart + item.Submarine.SimPosition, rayEnd + item.Submarine.SimPosition)); + hits.AddRange(DoRayCast(rayStart + item.Submarine.SimPosition, rayEnd + item.Submarine.SimPosition, submarine: null)); //also in the coordinate space of docked subs foreach (Submarine dockedSub in item.Submarine.DockedTo) { if (dockedSub == item.Submarine) { continue; } - hits.AddRange(DoRayCast(rayStart + item.Submarine.SimPosition - dockedSub.SimPosition, rayEnd + item.Submarine.SimPosition - dockedSub.SimPosition)); + hits.AddRange(DoRayCast(rayStart + item.Submarine.SimPosition - dockedSub.SimPosition, rayEnd + item.Submarine.SimPosition - dockedSub.SimPosition, dockedSub)); } } else @@ -350,7 +353,7 @@ namespace Barotrauma.Items.Components //shooting outdoors, see if we can hit anything inside a sub foreach (Submarine submarine in Submarine.Loaded) { - var inSubHits = DoRayCast(rayStart - submarine.SimPosition, rayEnd - submarine.SimPosition); + var inSubHits = DoRayCast(rayStart - submarine.SimPosition, rayEnd - submarine.SimPosition, submarine); //transform back to world coordinates for (int i = 0; i < inSubHits.Count; i++) { @@ -386,12 +389,20 @@ namespace Barotrauma.Items.Components } else { - Entity.Spawner.AddToRemoveQueue(item); + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) + { + //clients aren't allowed to remove items by themselves, so lets hide the projectile until the server tells us to remove it + item.HiddenInGame = Hitscan; + } + else + { + Entity.Spawner.AddToRemoveQueue(item); + } } } } - private List DoRayCast(Vector2 rayStart, Vector2 rayEnd) + private List DoRayCast(Vector2 rayStart, Vector2 rayEnd, Submarine submarine) { List hits = new List(); @@ -406,14 +417,20 @@ namespace Barotrauma.Items.Components if (fixture?.Body == null || fixture.IsSensor) { return true; } if (fixture.Body.UserData is VineTile) { return true; } if (fixture.Body.UserData is Item item && (item.GetComponent() == null && !item.Prefab.DamagedByProjectiles || item.Condition <= 0)) { return true; } - if (fixture.Body?.UserData as string == "ruinroom") { return true; } + if (fixture.Body.UserData as string == "ruinroom") { return true; } + + //if doing the raycast in a submarine's coordinate space, ignore anything that's not in that sub + if (submarine != null) + { + if (fixture.Body.UserData is Entity entity && entity.Submarine != submarine) { return true; } + } //ignore everything else than characters, sub walls and level walls if (!fixture.CollisionCategories.HasFlag(Physics.CollisionCharacter) && !fixture.CollisionCategories.HasFlag(Physics.CollisionWall) && !fixture.CollisionCategories.HasFlag(Physics.CollisionLevel)) { return true; } - if (fixture.Body.UserData is VoronoiCell && this.item.Submarine != null) { return true; } + if (fixture.Body.UserData is VoronoiCell && (this.item.Submarine != null || submarine != null)) { return true; } fixture.Body.GetTransform(out FarseerPhysics.Common.Transform transform); if (!fixture.Shape.TestPoint(ref transform, ref rayStart)) { return true; } @@ -436,7 +453,13 @@ namespace Barotrauma.Items.Components !fixture.CollisionCategories.HasFlag(Physics.CollisionWall) && !fixture.CollisionCategories.HasFlag(Physics.CollisionLevel)) { return -1; } - //ignore level cells if the item the point of impact are inside a sub + //if doing the raycast in a submarine's coordinate space, ignore anything that's not in that sub + if (submarine != null) + { + if (fixture.Body.UserData is Entity entity && entity.Submarine != submarine) { return -1; } + } + + //ignore level cells if the item and the point of impact are inside a sub if (fixture.Body.UserData is VoronoiCell && this.item.Submarine != null) { if (Hull.FindHull(ConvertUnits.ToDisplayUnits(point), this.item.CurrentHull) != null) @@ -536,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) { @@ -623,7 +646,7 @@ namespace Barotrauma.Items.Components { if (Attack != null) { attackResult = Attack.DoDamage(User, damageable, item.WorldPosition, 1.0f); } } - else if (target.Body.UserData is VoronoiCell voronoiCell && Attack != null && Math.Abs(Attack.StructureDamage) > 0.0f) + else if (target.Body.UserData is VoronoiCell voronoiCell && voronoiCell.IsDestructible && Attack != null && Math.Abs(Attack.StructureDamage) > 0.0f) { if (Level.Loaded?.ExtraWalls.Find(w => w.Body == target.Body) is DestructibleLevelWall destructibleWall) { @@ -633,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 @@ -642,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) @@ -672,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 @@ -741,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); @@ -756,7 +784,15 @@ namespace Barotrauma.Items.Components if (RemoveOnHit) { - Entity.Spawner?.AddToRemoveQueue(item); + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) + { + //clients aren't allowed to remove items by themselves, so lets hide the projectile until the server tells us to remove it + item.HiddenInGame = Hitscan; + } + else + { + Entity.Spawner?.AddToRemoveQueue(item); + } } return true; @@ -786,7 +822,8 @@ namespace Barotrauma.Items.Components { MotorEnabled = true, MaxMotorForce = 30.0f, - LimitEnabled = true + LimitEnabled = true, + Breakpoint = 1000.0f }; if (StickPermanently) 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..fb2cd53a7 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; } @@ -265,16 +269,19 @@ namespace Barotrauma.Items.Components ic.ReceiveSignal(stepsTaken, signal, recipient, source, sender, power, signalStrength); } - foreach (StatusEffect effect in recipient.Effects) + if (signal != "0") { - recipient.Item.ApplyStatusEffect(effect, ActionType.OnUse, (float)Timing.Step); + foreach (StatusEffect effect in recipient.Effects) + { + recipient.Item.ApplyStatusEffect(effect, ActionType.OnUse, (float)Timing.Step); + } } } } 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 +293,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 +307,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 +336,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 b3d11592a..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; @@ -21,6 +19,14 @@ namespace Barotrauma.Items.Components private List disconnectedWireIds; + /// + /// Allows rewiring the connection panel despite rewiring being disabled on a server + /// + public bool AlwaysAllowRewiring + { + get { return item.Submarine?.Info.Type == SubmarineType.BeaconStation; } + } + [Editable, Serialize(false, true, description: "Locked connection panels cannot be rewired in-game.", alwaysUseInstanceValues: true)] public bool Locked { @@ -103,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) @@ -122,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)) { @@ -176,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; } @@ -239,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/ExponentiationComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ExponentiationComponent.cs index 4d8fba217..b5c64f6e1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ExponentiationComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ExponentiationComponent.cs @@ -35,7 +35,7 @@ namespace Barotrauma.Items.Components break; case "signal_in": float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float value); - item.SendSignal(0, MathUtils.Pow(value, Exponent).ToString("G", CultureInfo.InvariantCulture), "signal_out", null); + item.SendSignal(stepsTaken, MathUtils.Pow(value, Exponent).ToString("G", CultureInfo.InvariantCulture), "signal_out", sender, source: source); break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/FunctionComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/FunctionComponent.cs index e1f02b23f..b6d7e856f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/FunctionComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/FunctionComponent.cs @@ -31,17 +31,17 @@ namespace Barotrauma.Items.Components public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0, float signalStrength = 1) { if (connection.Name != "signal_in") return; - if (!float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float value)) return; + if (!float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float value)) { return; } switch (Function) { case FunctionType.Round: - item.SendSignal(0, Math.Round(value).ToString("G", CultureInfo.InvariantCulture), "signal_out", null); + item.SendSignal(stepsTaken, Math.Round(value).ToString("G", CultureInfo.InvariantCulture), "signal_out", sender, source: source); break; case FunctionType.Ceil: - item.SendSignal(0, Math.Ceiling(value).ToString("G", CultureInfo.InvariantCulture), "signal_out", null); + item.SendSignal(stepsTaken, Math.Ceiling(value).ToString("G", CultureInfo.InvariantCulture), "signal_out", sender, source: source); break; case FunctionType.Floor: - item.SendSignal(0, Math.Floor(value).ToString("G", CultureInfo.InvariantCulture), "signal_out", null); + item.SendSignal(stepsTaken, Math.Floor(value).ToString("G", CultureInfo.InvariantCulture), "signal_out", sender, source: source); break; case FunctionType.Factorial: int intVal = (int)Math.Min(value, 20); @@ -50,15 +50,15 @@ namespace Barotrauma.Items.Components { factorial *= (ulong)i; } - item.SendSignal(0, factorial.ToString(), "signal_out", null); + item.SendSignal(stepsTaken, factorial.ToString(), "signal_out", sender, source: source); break; case FunctionType.AbsoluteValue: - item.SendSignal(0, Math.Abs(value).ToString("G", CultureInfo.InvariantCulture), "signal_out", null); + item.SendSignal(stepsTaken, Math.Abs(value).ToString("G", CultureInfo.InvariantCulture), "signal_out", sender, source: source); break; case FunctionType.SquareRoot: if (value > 0) { - item.SendSignal(0, Math.Sqrt(value).ToString("G", CultureInfo.InvariantCulture), "signal_out", null); + item.SendSignal(stepsTaken, Math.Sqrt(value).ToString("G", CultureInfo.InvariantCulture), "signal_out", sender, source: source); } break; default: 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/ModuloComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ModuloComponent.cs index a66276988..7e8d15874 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ModuloComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ModuloComponent.cs @@ -32,7 +32,7 @@ namespace Barotrauma.Items.Components break; case "signal_in": float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float value); - item.SendSignal(0, (value % modulus).ToString("G", CultureInfo.InvariantCulture), "signal_out", null); + item.SendSignal(stepsTaken, (value % modulus).ToString("G", CultureInfo.InvariantCulture), "signal_out", sender, source: source); break; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs index 9f10c0973..ae3627f7e 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,20 @@ namespace Barotrauma.Items.Components foreach (Character c in Character.CharacterList) { if (IgnoreDead && c.IsDead) { continue; } - if (OnlyHumans && !c.IsHuman) { continue; } + + //ignore characters that have spawned a second or less ago + //makes it possible to detect when a spawned character moves without triggering the detector immediately as the ragdoll spawns and drops to the ground + if (c.SpawnTime > Timing.TotalTime - 1.0) { continue; } + + switch (Target) + { + case TargetType.Human: + if (!c.IsHuman) { continue; } + break; + case TargetType.Monster: + if (c.IsHuman || c.IsPet) { continue; } + break; + } //do a rough check based on the position of the character's collider first //before the more accurate limb-based check 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/SignalCheckComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SignalCheckComponent.cs index eb725383d..cd59dbc7c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SignalCheckComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SignalCheckComponent.cs @@ -25,7 +25,7 @@ namespace Barotrauma.Items.Components string signalOut = (signal == TargetSignal) ? Output : FalseOutput; if (string.IsNullOrWhiteSpace(signalOut)) return; - item.SendSignal(stepsTaken, signalOut, "signal_out", sender, signalStrength); + item.SendSignal(stepsTaken, signalOut, "signal_out", sender, signalStrength, source); break; case "set_output": diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs index 53251fdf4..415f81beb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs @@ -68,18 +68,18 @@ namespace Barotrauma.Items.Components { case FunctionType.Sin: if (!UseRadians) { value = MathHelper.ToRadians(value); } - item.SendSignal(0, ((float)Math.Sin(value)).ToString("G", CultureInfo.InvariantCulture), "signal_out", null); + item.SendSignal(stepsTaken, ((float)Math.Sin(value)).ToString("G", CultureInfo.InvariantCulture), "signal_out", sender, source: source); break; case FunctionType.Cos: if (!UseRadians) { value = MathHelper.ToRadians(value); } - item.SendSignal(0, ((float)Math.Cos(value)).ToString("G", CultureInfo.InvariantCulture), "signal_out", null); + item.SendSignal(stepsTaken, ((float)Math.Cos(value)).ToString("G", CultureInfo.InvariantCulture), "signal_out", sender, source: source); break; case FunctionType.Tan: if (!UseRadians) { value = MathHelper.ToRadians(value); } //tan is undefined if the value is (π / 2) + πk, where k is any integer if (!MathUtils.NearlyEqual(value % MathHelper.Pi, MathHelper.PiOver2)) { - item.SendSignal(0, ((float)Math.Tan(value)).ToString("G", CultureInfo.InvariantCulture), "signal_out", null); + item.SendSignal(stepsTaken, ((float)Math.Tan(value)).ToString("G", CultureInfo.InvariantCulture), "signal_out", sender, source: source); } break; case FunctionType.Asin: @@ -88,7 +88,7 @@ namespace Barotrauma.Items.Components { float angle = (float)Math.Asin(value); if (!UseRadians) { angle = MathHelper.ToDegrees(angle); } - item.SendSignal(0, angle.ToString("G", CultureInfo.InvariantCulture), "signal_out", null); + item.SendSignal(stepsTaken, angle.ToString("G", CultureInfo.InvariantCulture), "signal_out", sender, source: source); } break; case FunctionType.Acos: @@ -97,7 +97,7 @@ namespace Barotrauma.Items.Components { float angle = (float)Math.Acos(value); if (!UseRadians) { angle = MathHelper.ToDegrees(angle); } - item.SendSignal(0, angle.ToString("G", CultureInfo.InvariantCulture), "signal_out", null); + item.SendSignal(stepsTaken, angle.ToString("G", CultureInfo.InvariantCulture), "signal_out", sender, source: source); } break; case FunctionType.Atan: @@ -115,7 +115,7 @@ namespace Barotrauma.Items.Components { float angle = (float)Math.Atan(value); if (!UseRadians) { angle = MathHelper.ToDegrees(angle); } - item.SendSignal(0, angle.ToString("G", CultureInfo.InvariantCulture), "signal_out", null); + item.SendSignal(stepsTaken, angle.ToString("G", CultureInfo.InvariantCulture), "signal_out", sender, source: source); } break; default: diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs index f3f8911f4..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 @@ -152,7 +152,7 @@ namespace Barotrauma.Items.Components channelMemory[index] = MathHelper.Clamp(value, 0, 10000); } - public void TransmitSignal(int stepsTaken, string signal, Item source, Character sender, bool sendToChat, float signalStrength = 1.0f) + public void TransmitSignal(int stepsTaken, string signal, Item source, Character sender, bool sentFromChat, float signalStrength = 1.0f) { var senderComponent = source?.GetComponent(); if (senderComponent != null && !CanReceive(senderComponent)) { return; } @@ -162,6 +162,8 @@ namespace Barotrauma.Items.Components var receivers = GetReceiversInRange(); foreach (WifiComponent wifiComp in receivers) { + if (sentFromChat && !wifiComp.LinkToChat) { continue; } + //signal strength diminishes by distance float sentSignalStrength = signalStrength * MathHelper.Clamp(1.0f - (Vector2.Distance(item.WorldPosition, wifiComp.item.WorldPosition) / wifiComp.range), 0.0f, 1.0f); @@ -176,11 +178,12 @@ namespace Barotrauma.Items.Components source.LastSentSignalRecipients.Add(receiverItem); } } - } + } - if (DiscardDuplicateChatMessages && signal == prevSignal) continue; + if (DiscardDuplicateChatMessages && signal == prevSignal) { continue; } - if (LinkToChat && wifiComp.LinkToChat && chatMsgCooldown <= 0.0f && sendToChat) + //create a chat message + if (LinkToChat && wifiComp.LinkToChat && chatMsgCooldown <= 0.0f && !sentFromChat) { if (wifiComp.item.ParentInventory != null && wifiComp.item.ParentInventory.Owner != null) @@ -232,7 +235,7 @@ namespace Barotrauma.Items.Components switch (connection.Name) { case "signal_in": - TransmitSignal(stepsTaken, signal, source, sender, true, signalStrength); + TransmitSignal(stepsTaken, signal, source, sender, false, signalStrength); break; case "set_channel": if (int.TryParse(signal, out int newChannel)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs index e32534946..af86d2973 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs @@ -37,6 +37,11 @@ namespace Barotrauma.Items.Components angle = MathUtils.VectorToAngle(end - start); length = Vector2.Distance(start, end); + + if (length > 5000.0f) + { + int akjsdnfkjsadf = 1; + } } } @@ -183,8 +188,12 @@ namespace Barotrauma.Items.Components if (refSub == null) { Structure attachTarget = Structure.GetAttachTarget(newConnection.Item.WorldPosition); - if (attachTarget == null) { continue; } - refSub = attachTarget.Submarine; + if (attachTarget == null && !(newConnection.Item.GetComponent()?.Attached ?? false)) + { + connections[i] = null; + continue; + } + refSub = attachTarget?.Submarine; } Vector2 nodePos = refSub == null ? @@ -238,18 +247,18 @@ namespace Barotrauma.Items.Components { foreach (ItemComponent ic in item.Components) { - if (ic == this) continue; + if (ic == this) { continue; } ic.Drop(null); } - if (item.Container != null) item.Container.RemoveContained(this.item); - if (item.body != null) item.body.Enabled = false; + if (item.Container != null) { item.Container.RemoveContained(this.item); } + if (item.body != null) { item.body.Enabled = false; } IsActive = false; CleanNodes(); } - - if (item.body != null) item.Submarine = newConnection.Item.Submarine; + + if (item.body != null) { item.Submarine = newConnection.Item.Submarine; } if (sendNetworkEvent) { @@ -735,6 +744,11 @@ namespace Barotrauma.Items.Components public override void FlipX(bool relativeToSub) { if (item.ParentInventory != null) { return; } +#if CLIENT + if (!relativeToSub && Screen.Selected != GameMain.SubEditorScreen) { return; } +#else + if (!relativeToSub) { return; } +#endif Vector2 refPos = item.Submarine == null ? Vector2.Zero : diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index 2f91a277f..6309d6935 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -76,7 +76,7 @@ namespace Barotrauma.Items.Components set { launchImpulse = value; } } - [Editable(0.0f, 1000.0f), Serialize(5.0f, false, description: "The period of time the user has to wait between shots.")] + [Editable(0.0f, 1000.0f, decimals: 3), Serialize(5.0f, false, description: "The period of time the user has to wait between shots.")] public float Reload { get { return reloadTime; } @@ -198,6 +198,7 @@ namespace Barotrauma.Items.Components private set; } + private float prevScale; float prevBaseRotation; [Serialize(0.0f, true, description: "The angle of the turret's base in degrees.", alwaysUseInstanceValues: true)] public float BaseRotation @@ -250,33 +251,42 @@ namespace Barotrauma.Items.Components private void UpdateTransformedBarrelPos() { - float flippedRotation = item.Rotation; - if (item.FlippedX) { flippedRotation = -flippedRotation; } - //if (item.FlippedY) flippedRotation = 180.0f - flippedRotation; - transformedBarrelPos = MathUtils.RotatePointAroundTarget(barrelPos * item.Scale, new Vector2(item.Rect.Width / 2, item.Rect.Height / 2), flippedRotation); + transformedBarrelPos = MathUtils.RotatePointAroundTarget(barrelPos * item.Scale, new Vector2(item.Rect.Width / 2, item.Rect.Height / 2), MathHelper.ToRadians(item.Rotation)); #if CLIENT item.ResetCachedVisibleSize(); #endif - item.Rotation = flippedRotation; prevBaseRotation = item.Rotation; + prevScale = item.Scale; } - public override void OnItemLoaded() + public override void OnMapLoaded() { - base.OnItemLoaded(); - var lightComponents = item.GetComponents(); - if (lightComponents != null && lightComponents.Count() > 0) + base.OnMapLoaded(); + FindLightComponent(); + if (loadedRotationLimits.HasValue) { RotationLimits = loadedRotationLimits.Value; } + if (loadedBaseRotation.HasValue) { BaseRotation = loadedBaseRotation.Value; } + UpdateTransformedBarrelPos(); + } + + private void FindLightComponent() + { + foreach (LightComponent lc in item.GetComponents()) { - lightComponent = lightComponents.FirstOrDefault(lc => lc.Parent == this); -#if CLIENT - if (lightComponent != null) + if (lc?.Parent == this) { - lightComponent.Parent = null; - lightComponent.Rotation = Rotation - MathHelper.ToRadians(item.Rotation); - lightComponent.Light.Rotation = -rotation; + lightComponent = lc; + break; } -#endif } + +#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) @@ -284,7 +294,7 @@ namespace Barotrauma.Items.Components this.cam = cam; if (reload > 0.0f) { reload -= deltaTime; } - if (!MathUtils.NearlyEqual(item.Rotation, prevBaseRotation)) + if (!MathUtils.NearlyEqual(item.Rotation, prevBaseRotation) || !MathUtils.NearlyEqual(item.Scale, prevScale)) { UpdateTransformedBarrelPos(); } @@ -303,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); @@ -325,28 +339,56 @@ 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); + + float targetRotationDiff = MathHelper.WrapAngle(targetRotation - rotation); + + if ((maxRotation - minRotation) < MathHelper.TwoPi) + { + float targetRotationMaxDiff = MathHelper.WrapAngle(targetRotation - maxRotation); + float targetRotationMinDiff = MathHelper.WrapAngle(targetRotation - minRotation); + + if (Math.Abs(targetRotationMaxDiff) < Math.Abs(targetRotationMinDiff) && + rotMidDiff < 0.0f && + targetRotationDiff < 0.0f) + { + targetRotationDiff += MathHelper.TwoPi; + } + else if (Math.Abs(targetRotationMaxDiff) > Math.Abs(targetRotationMinDiff) && + rotMidDiff > 0.0f && + targetRotationDiff > 0.0f) + { + targetRotationDiff -= MathHelper.TwoPi; + } } angularVelocity += - (MathHelper.WrapAngle(targetRotation - rotation) * springStiffness - angularVelocity * springDamping) * deltaTime; + (targetRotationDiff * springStiffness - angularVelocity * springDamping) * deltaTime; angularVelocity = MathHelper.Clamp(angularVelocity, -rotationSpeed, rotationSpeed); rotation += angularVelocity * deltaTime; - float rotMidDiff = MathHelper.WrapAngle(rotation - (minRotation + maxRotation) / 2.0f); + rotMidDiff = MathHelper.WrapAngle(rotation - (minRotation + maxRotation) / 2.0f); if (rotMidDiff < -maxDist) { rotation = minRotation; angularVelocity *= -0.5f; - } + } else if (rotMidDiff > maxDist) { rotation = maxRotation; angularVelocity *= -0.5f; } + UpdateLightComponent(); + } + + private void UpdateLightComponent() + { if (lightComponent != null) { lightComponent.Rotation = Rotation - MathHelper.ToRadians(item.Rotation); @@ -667,10 +709,7 @@ namespace Barotrauma.Items.Components } else { - 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) { return; } + if (!CheckTurretAngle(angle)) { return; } float enemyAngle = MathUtils.VectorToAngle(target.WorldPosition - item.WorldPosition); float turretAngle = -rotation; if (Math.Abs(MathUtils.GetShortestAngle(enemyAngle, turretAngle)) > 0.15f) { return; } @@ -685,7 +724,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) @@ -724,12 +767,13 @@ namespace Barotrauma.Items.Components TryLaunch(deltaTime, ignorePower: true); } + private bool outOfAmmo; public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { 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); } @@ -741,7 +785,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; @@ -762,18 +806,16 @@ 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(); + if (container != null) { - 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; + } } } @@ -785,39 +827,44 @@ 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; } } if (container == null || container.ContainableItems.Count == 0) { - character.Speak(TextManager.GetWithVariable("DialogCannotLoadTurret", "[itemname]", item.Name, true), null, 0.0f, "cannotloadturret", 30.0f); + if (!outOfAmmo && character.IsOnPlayerTeam) + { + character.Speak(TextManager.GetWithVariable("DialogCannotLoadTurret", "[itemname]", item.Name, true), null, 0.0f, "cannotloadturret", 30.0f); + } return true; } if (objective.SubObjectives.None()) { - if (!AIDecontainEmptyItems(character, objective, equip: true, sourceContainer: container)) + var loadItemsObjective = AIContainItems(container, character, objective, usableProjectileCount + 1, equip: true, removeEmpty: true, dropItemOnDeselected: true); + loadItemsObjective.ignoredContainerIdentifiers = new string[] { containerItem.prefab.Identifier }; + if (character.IsOnPlayerTeam) { - return false; - } - } - if (objective.SubObjectives.None()) - { - var loadItemsObjective = AIContainItems(container, character, objective, usableProjectileCount + 1, equip: true, removeEmpty: true); - if (loadItemsObjective == null) - { - if (usableProjectileCount == 0) - { - character.Speak(TextManager.GetWithVariable("DialogCannotLoadTurret", "[itemname]", item.Name, true), null, 0.0f, "cannotloadturret", 30.0f); - return true; - } - } - else - { - loadItemsObjective.ignoredContainerIdentifiers = new string[] { containerItem.prefab.Identifier }; character.Speak(TextManager.GetWithVariable("DialogLoadTurret", "[itemname]", item.Name, true), null, 0.0f, "loadturret", 30.0f); - return false; + } + loadItemsObjective.Abandoned += CheckRemainingAmmo; + loadItemsObjective.Completed += CheckRemainingAmmo; + return outOfAmmo; + + void CheckRemainingAmmo() + { + if (!character.IsOnPlayerTeam) { return; } + string ammoType = container.Item.HasTag("railgunammosource") ? "railgunammo" : container.Item.HasTag("coilgunammosource") ? "coilgunammo" : "turretammo"; + int remainingAmmo = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(ammoType) && i.Condition > 1); + if (remainingAmmo == 0) + { + character.Speak(TextManager.Get($"DialogOutOf{ammoType}"), null, 0.0f, "outofammo", 30.0f); + } + else if (remainingAmmo < 3) + { + character.Speak(TextManager.Get($"DialogLowOn{ammoType}"), null, 0.0f, "outofammo", 30.0f); + } } } if (objective.SubObjectives.Any()) @@ -828,53 +875,166 @@ namespace Barotrauma.Items.Components //enough shells and power Character closestEnemy = null; - float closestDist = AIRange * AIRange; + Vector2? targetPos = null; + float maxDistance = 10000; + float shootDistance = AIRange * item.OffsetOnSelectedMultiplier; + float closestDistance = maxDistance * maxDistance; 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; } - + // Don't aim monsters that are inside a submarine. + if (!enemy.IsHuman && enemy.CurrentHull != null) { 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 (dist < shootDistance * shootDistance) + { + // Only check the angle to targets that are close enough to be shot at + // We shouldn't check the angle when a long creature is traveling outside of the shooting range, because doing so would not allow us to shoot the limbs that might be close enough to shoot at. + if (!CheckTurretAngle(enemy.WorldPosition)) { continue; } + } closestEnemy = enemy; - closestDist = dist; + closestDistance = dist; } - if (closestEnemy == null) { return false; } - - character.AIController.SelectTarget(closestEnemy.AiTarget); + if (closestEnemy != null) + { + // Target the closest limb. Doesn't make much difference with smaller creatures, but enables the bots to shoot longer abyss creatures like the endworm. Otherwise they just target the main body = head. + targetPos = closestEnemy.WorldPosition; + float closestDist = closestDistance; + foreach (Limb limb in closestEnemy.AnimController.Limbs) + { + if (limb.IsSevered) { continue; } + if (limb.Hidden) { continue; } + if (!CheckTurretAngle(limb.WorldPosition)) { continue; } + float dist = Vector2.DistanceSquared(limb.WorldPosition, item.WorldPosition); + if (dist < closestDist) + { + closestDist = dist; + targetPos = limb.WorldPosition; + } + } + if (closestDist > shootDistance * shootDistance) + { + // Not close enough to shoot + closestEnemy = null; + targetPos = null; + } + } + 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.IsOnPlayerTeam) + { + 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.IsOnPlayerTeam) + { + 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) @@ -905,17 +1065,30 @@ 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); + if (character.IsOnPlayerTeam) + { + character.Speak(TextManager.Get("DialogFireTurret"), null, 0.0f, "fireturret", 10.0f); + } character.SetInput(InputType.Shoot, true, true); return false; } + private bool CheckTurretAngle(float angle) + { + 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; + } + + private bool CheckTurretAngle(Vector2 target) => CheckTurretAngle(-MathUtils.VectorToAngle(target - item.WorldPosition)); + protected override void RemoveComponentSpecific() { base.RemoveComponentSpecific(); @@ -960,7 +1133,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(); @@ -997,7 +1169,22 @@ namespace Barotrauma.Items.Components public override void FlipY(bool relativeToSub) { - BaseRotation = MathHelper.ToDegrees(MathUtils.WrapAngleTwoPi(MathHelper.ToRadians(BaseRotation - 180))); + BaseRotation = MathHelper.ToDegrees(MathUtils.WrapAngleTwoPi(MathHelper.ToRadians(180 - BaseRotation))); + + minRotation = -minRotation; + maxRotation = -maxRotation; + + var temp = minRotation; + minRotation = maxRotation; + maxRotation = temp; + + while (minRotation < 0) + { + minRotation += MathHelper.TwoPi; + maxRotation += MathHelper.TwoPi; + } + rotation = (minRotation + maxRotation) / 2; + UpdateTransformedBarrelPos(); } @@ -1016,6 +1203,8 @@ namespace Barotrauma.Items.Components resetUserTimer = 10.0f; break; case "trigger_in": + if (signal == "0") { return; } + lightComponent.IsOn = !lightComponent.IsOn; item.Use((float)Timing.Step, sender); user = sender; resetUserTimer = 10.0f; @@ -1041,6 +1230,26 @@ namespace Barotrauma.Items.Components } } + private Vector2? loadedRotationLimits; + private float? loadedBaseRotation; + public override void Load(XElement componentElement, bool usePrefabValues, IdRemap idRemap) + { + base.Load(componentElement, usePrefabValues, idRemap); + loadedRotationLimits = componentElement.GetAttributeVector2("rotationlimits", RotationLimits); + loadedBaseRotation = componentElement.GetAttributeFloat("baserotation", componentElement.Parent.GetAttributeFloat("rotation", BaseRotation)); + } + + public override void OnItemLoaded() + { + base.OnItemLoaded(); + FindLightComponent(); + if (!loadedBaseRotation.HasValue) + { + if (item.FlippedX) { FlipX(relativeToSub: false); } + if (item.FlippedY) { FlipY(relativeToSub: false); } + } + } + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { if (extraData.Length > 2) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs index 6963fd045..2ad634ba9 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) @@ -419,13 +773,12 @@ namespace Barotrauma return match; } - public List FindAllItems(Func predicate, bool recursive = false, List list = null) + public List FindAllItems(Func predicate = null, 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)) + if (predicate == null || 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 5828dcc47..bf0041f46 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)] @@ -411,11 +448,10 @@ namespace Barotrauma get { return condition; } set { -#if CLIENT - if (GameMain.Client != null) return; -#endif - if (!MathUtils.IsValid(value)) return; - if (Indestructible) return; + 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; @@ -428,7 +464,7 @@ namespace Barotrauma { ic.PlaySound(ActionType.OnBroken); } - if (Screen.Selected == GameMain.SubEditorScreen) return; + if (Screen.Selected == GameMain.SubEditorScreen) { return; } #endif ApplyStatusEffects(ActionType.OnBroken, 1.0f, null); } @@ -468,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; @@ -568,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; } } @@ -591,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; } } @@ -645,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( @@ -841,12 +874,14 @@ namespace Barotrauma DebugConsole.Log("Created " + Name + " (" + ID + ")"); - if (Components.Any() && Components.All(ic => ic is Wire || ic is Holdable)) { isWire = true; } + if (Components.Any(ic => ic is Wire) && Components.All(ic => ic is Wire || ic is Holdable)) { isWire = true; } if (HasTag("logic")) { isLogic = true; } } 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) @@ -901,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; } @@ -1143,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; @@ -1159,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; @@ -1182,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() { @@ -1310,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); } } @@ -1387,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; @@ -1577,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) { @@ -1666,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); } + } } } @@ -1690,6 +1712,11 @@ namespace Barotrauma flippedX = false; return; } + + if (Prefab.AllowRotatingInEditor) + { + rotationRad = MathUtils.WrapAngleTwoPi(-rotationRad); + } #if CLIENT if (Prefab.CanSpriteFlipX) { @@ -1872,7 +1899,7 @@ namespace Barotrauma { if (connections == null) { return; } if (!connections.TryGetValue(connectionName, out Connection c)) { return; } - SendSignal(stepsTaken, signal, c, sender, power, source, signalStrength); + SendSignal(stepsTaken, signal, c, sender, power, source ?? this, signalStrength); } public void SendSignal(int stepsTaken, string signal, Connection connection, Character sender, float power = 0.0f, Item source = null, float signalStrength = 1.0f) @@ -1884,6 +1911,14 @@ namespace Barotrauma if (stepsTaken > 10) { + //if the signal has been passed through this item multiple times already, interrupt it to prevent infinite loops + if (source != null) + { + if (source.LastSentSignalRecipients.Count(recipient => recipient == this) > 2) + { + return; + } + } //use a coroutine to prevent infinite loops by creating a one //frame delay if the "signal chain" gets too long CoroutineManager.StartCoroutine(SendSignal(signal, connection, sender, power, signalStrength)); @@ -1908,11 +1943,6 @@ namespace Barotrauma yield return CoroutineStatus.Success; } - public float GetDrawDepth() - { - return SpriteDepth + ((ID % 255) * 0.000001f); - } - public bool IsInsideTrigger(Vector2 worldPosition) { return IsInsideTrigger(worldPosition, out _); @@ -1943,7 +1973,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; @@ -2047,24 +2077,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; } @@ -2259,7 +2284,6 @@ namespace Barotrauma public void Unequip(Character character) { - character.DeselectItem(this); foreach (ItemComponent ic in components) { ic.Unequip(character); } } @@ -2696,11 +2720,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() @@ -2747,11 +2778,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 e37ac8fda..499f84a9b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -7,6 +7,7 @@ using System.Xml.Linq; using System.Linq; using Barotrauma.Items.Components; using Barotrauma.Extensions; +using Voronoi2; namespace Barotrauma { @@ -21,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) { @@ -29,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); } } @@ -65,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) { @@ -78,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()) { @@ -117,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)); @@ -136,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)); @@ -156,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; } @@ -167,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) { @@ -185,7 +195,7 @@ namespace Barotrauma } } - partial class ItemPrefab : MapEntityPrefab + partial class ItemPrefab : MapEntityPrefab, IHasUintIdentifier { private readonly string name; public override string Name => name; @@ -205,7 +215,7 @@ namespace Barotrauma protected Vector2 size; private float impactTolerance; - private readonly PriceInfo defaultPrice; + public readonly PriceInfo DefaultPrice; private readonly Dictionary locationPrices; /// @@ -465,6 +475,24 @@ namespace Barotrauma private set; } = new Dictionary(); + public Dictionary LevelQuantity + { + get; + } = new Dictionary(); + + public struct FixedQuantityResourceInfo + { + public int ClusterQuantity { get; } + public int ClusterSize { get; } + public bool IsIslandSpecifc { get; } + + public FixedQuantityResourceInfo(int clusterQuantity, int clusterSize, bool isIslandSpecific) + { + ClusterQuantity = clusterQuantity; + ClusterSize = clusterSize; + IsIslandSpecifc = isIslandSpecific; + } + } [Serialize(true, false)] public bool CanFlipX { get; private set; } @@ -479,9 +507,28 @@ 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)); + 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 uint UIntIdentifier { get; set; } public static void RemoveByFile(string filePath) => Prefabs.RemoveByFile(filePath); @@ -597,11 +644,13 @@ namespace Barotrauma originalName = element.GetAttributeString("name", ""); name = originalName; identifier = element.GetAttributeString("identifier", ""); - if (!Enum.TryParse(element.GetAttributeString("category", "Misc"), true, out MapEntityCategory category)) + + string categoryStr = element.GetAttributeString("category", "Misc"); + if (!Enum.TryParse(categoryStr, true, out MapEntityCategory category)) { category = MapEntityCategory.Misc; } - Category = category; + Category = category; var parentType = element.Parent?.GetAttributeString("itemtype", "") ?? string.Empty; @@ -725,7 +774,7 @@ namespace Barotrauma if (locationPrices == null) { locationPrices = new Dictionary(); } if (subElement.Attribute("baseprice") != null) { - foreach (Tuple priceInfo in PriceInfo.CreatePriceInfos(subElement, out defaultPrice)) + foreach (Tuple priceInfo in PriceInfo.CreatePriceInfos(subElement, out DefaultPrice)) { if (priceInfo == null) { continue; } locationPrices.Add(priceInfo.Item1, priceInfo.Item2); @@ -857,6 +906,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) @@ -864,10 +915,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": @@ -898,12 +948,25 @@ namespace Barotrauma break; case "levelresource": - foreach (XElement levelCommonnessElement in subElement.Elements()) + foreach (XElement levelCommonnessElement in subElement.GetChildElements("commonness")) { string levelName = levelCommonnessElement.GetAttributeString("leveltype", "").ToLowerInvariant(); - if (!LevelCommonness.ContainsKey(levelName)) + if (!levelCommonnessElement.GetAttributeBool("fixedquantity", false)) { - LevelCommonness.Add(levelName, levelCommonnessElement.GetAttributeFloat("commonness", 0.0f)); + if (!LevelCommonness.ContainsKey(levelName)) + { + LevelCommonness.Add(levelName, levelCommonnessElement.GetAttributeFloat("commonness", 0.0f)); + } + } + else + { + if (!LevelQuantity.ContainsKey(levelName)) + { + LevelQuantity.Add(levelName, new FixedQuantityResourceInfo( + levelCommonnessElement.GetAttributeInt("clusterquantity", 0), + levelCommonnessElement.GetAttributeInt("clustersize", 0), + levelCommonnessElement.GetAttributeBool("isislandspecific", false))); + } } } break; @@ -926,7 +989,14 @@ namespace Barotrauma // with separate Price elements and there is no default price explicitly set if (locationPrices != null && locationPrices.Any()) { - defaultPrice ??= new PriceInfo(GetMinPrice() ?? 0, false); + DefaultPrice ??= new PriceInfo(GetMinPrice() ?? 0, false); + } + + //backwards compatibility + if (categoryStr.Equals("Thalamus", StringComparison.OrdinalIgnoreCase)) + { + Category = MapEntityCategory.Wrecked; + Subcategory = "Thalamus"; } if (sprite == null) @@ -964,6 +1034,7 @@ namespace Barotrauma AllowedLinks = element.GetAttributeStringArray("allowedlinks", new string[0], convertToLowerInvariant: true).ToList(); Prefabs.Add(this, allowOverriding); + this.CalculatePrefabUIntIdentifier(Prefabs); } public float GetTreatmentSuitability(string treatmentIdentifier) @@ -981,7 +1052,7 @@ namespace Barotrauma } else { - return defaultPrice; + return DefaultPrice; } } @@ -1026,9 +1097,9 @@ namespace Barotrauma int? minPrice = locationPrices != null && locationPrices.Values.Any() ? locationPrices?.Values.Min(p => p.Price) : null; if (minPrice.HasValue) { - if (defaultPrice != null) + if (DefaultPrice != null) { - return minPrice < defaultPrice.Price ? minPrice : defaultPrice.Price; + return minPrice < DefaultPrice.Price ? minPrice : DefaultPrice.Price; } else { @@ -1037,36 +1108,38 @@ namespace Barotrauma } else { - return defaultPrice?.Price; + return DefaultPrice?.Price; } } - 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 10952ac54..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,13 +202,18 @@ 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; } + + [Serialize("", true, "What sound to play when the ballast flora bursts thru walls")] + public string BurstSound { get; set; } = ""; private float availablePower; - private float toxinsTimer; - + [Serialize(0f, true, "How much power the ballast flora has stored.")] public float AvailablePower { get => availablePower; @@ -244,7 +252,7 @@ namespace Barotrauma.MapCreatures.Behavior public float PowerConsumptionTimer; private float defenseCooldown, toxinsCooldown, fireCheckCooldown; - private float damageIndicatorTimer, selfDamageTimer; + private float damageIndicatorTimer, selfDamageTimer, toxinsTimer; private readonly List branchesVulnerableToFire = new List(); @@ -293,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); @@ -373,8 +382,8 @@ namespace Barotrauma.MapCreatures.Behavior int flowerConfig = getInt("flowerconfig"); int leafconfig = getInt("leafconfig"); int id = getInt("ID"); - int health = getInt("health"); - int maxhealth = getInt("maxhealth"); + float health = getFloat("health"); + float maxhealth = getFloat("maxhealth"); int sides = getInt("sides"); int blockedSides = getInt("blockedsides"); int claimedId = branchElement.GetAttributeInt("claimed", -1); @@ -398,6 +407,7 @@ namespace Barotrauma.MapCreatures.Behavior Branches.Add(newBranch); int getInt(string name) => branchElement.GetAttributeInt(name, 0); + float getFloat(string name) => branchElement.GetAttributeFloat(name, 0f); } } @@ -424,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); @@ -466,24 +477,8 @@ namespace Barotrauma.MapCreatures.Behavior } } } - else - { - if (selfDamageTimer <= 0) - { - if (!CanGrowMore()) - { - foreach (BallastFloraBranch branch in Branches) - { - float maxHealth = branch.IsRoot ? StemHealth : BranchHealth; - DamageBranch(branch, Rand.Range(1f, maxHealth), AttackType.Other); - } - } - - selfDamageTimer = 1f; - } - - selfDamageTimer -= deltaTime; - } + + UpdateSelfDamage(deltaTime); if (Anger > 1f) { @@ -544,6 +539,41 @@ namespace Barotrauma.MapCreatures.Behavior } } + private void UpdateSelfDamage(float deltaTime) + { + if (selfDamageTimer <= 0) + { + bool hasRoot = false; + foreach (BallastFloraBranch branch in Branches) + { + if (branch.IsRoot) + { + hasRoot = true; + break; + } + } + + if (!hasRoot) + { + Kill(); + return; + } + + if (!HasBrokenThrough && !CanGrowMore()) + { + Branches.ForEachMod(branch => + { + float maxHealth = branch.IsRoot ? StemHealth : BranchHealth; + DamageBranch(branch, Rand.Range(1f, maxHealth), AttackType.Other); + }); + } + + selfDamageTimer = 1f; + } + + selfDamageTimer -= deltaTime; + } + private void UpdatePowerDrain(float deltaTime) { PowerConsumptionTimer += deltaTime; @@ -576,7 +606,7 @@ namespace Barotrauma.MapCreatures.Behavior float batteryDrain = powerDelta * 0.1f; foreach (PowerContainer battery in ClaimedBatteries) { - float amount = Math.Max(battery.MaxOutPut, batteryDrain); + float amount = Math.Min(battery.MaxOutPut, batteryDrain); if (battery.Charge > amount) { @@ -744,7 +774,7 @@ namespace Barotrauma.MapCreatures.Behavior #if SERVER if (!load) { - SendNetworkMessage(this, NetworkHeader.Infect, target.ID, true); + SendNetworkMessage(this, NetworkHeader.Infect, target.ID, true, branch); } #endif } @@ -802,7 +832,7 @@ namespace Barotrauma.MapCreatures.Behavior Vector2 flowerPos = GetWorldPosition() + newBranch.Position; CreateShapnel(flowerPos); newBranch.GrowthStep = 2.0f; - SoundPlayer.PlayDamageSound("ArmorBreak", 1.0f, flowerPos, range: 800); + SoundPlayer.PlayDamageSound(BurstSound, 1.0f, flowerPos, range: 800); } #endif } @@ -840,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; } @@ -853,7 +884,7 @@ namespace Barotrauma.MapCreatures.Behavior { if (IsInWater(branch)) { - return; + damage *= 1f - SubmergedWaterResistance; } if (defenseCooldown <= 0) @@ -861,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) @@ -901,6 +932,8 @@ namespace Barotrauma.MapCreatures.Behavior { target.Infector = null; } + + _entityList.Remove(this); } public void RemoveBranch(BallastFloraBranch branch) @@ -935,18 +968,10 @@ namespace Barotrauma.MapCreatures.Behavior } } } - }); + }); #if CLIENT - Vector2 pos = GetWorldPosition() + branch.Position; - - GameMain.ParticleManager.CreateParticle("bloodsplash", pos, Rand.Range(0, 360), Rand.Range(0, 100)); - GameMain.ParticleManager.CreateParticle("waterblood", pos, Rand.Range(0, 360), 0); - - for (int i = 0; i < 4; i++) - { - GameMain.ParticleManager.CreateParticle("gib", pos, Rand.Range(0, 360), Rand.Range(100f, 300f)); - } + CreateDeathParticle(branch); #endif if (isClient) { return; } @@ -1014,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) { @@ -1041,7 +1068,7 @@ namespace Barotrauma.MapCreatures.Behavior CreateShapnel(GetWorldPosition() + branch.Position); } - SoundPlayer.PlayDamageSound("ArmorBreak", BreakthroughPoint, GetWorldPosition(), range: 800); + SoundPlayer.PlayDamageSound(BurstSound, BreakthroughPoint, GetWorldPosition(), range: 800); #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/DefendWithPumpState.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/DefendWithPumpState.cs index e9be32c76..ab939cc7e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/DefendWithPumpState.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/DefendWithPumpState.cs @@ -51,6 +51,7 @@ namespace Barotrauma.MapCreatures.Behavior if (pump.Item.CurrentHull == targetBranch.CurrentHull) { targetPumps.Add(pump); + SetPump(pump); pump.Hijacked = true; } } @@ -90,18 +91,23 @@ namespace Barotrauma.MapCreatures.Behavior } } + private void SetPump(Pump pump) + { + if (pump.TargetLevel != null) + { + pump.TargetLevel = 100f; + } + else + { + pump.FlowPercentage = 100f; + } + } + public void Update(float deltaTime) { foreach (Pump pump in targetPumps) { - if (pump.TargetLevel != null) - { - pump.TargetLevel = 100f; - } - else - { - pump.FlowPercentage = 100f; - } + SetPump(pump); } if (tryDrown && !filled) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/GrowToTargetState.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/GrowToTargetState.cs index 251ad8abb..4d30f0f94 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/GrowToTargetState.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/GrowToTargetState.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using Barotrauma.Items.Components; using Microsoft.Xna.Framework; @@ -25,6 +26,17 @@ namespace Barotrauma.MapCreatures.Behavior protected override void Grow() { + if (TargetBranches.Any(b => b.Removed)) + { + if (!Behavior.IgnoredTargets.ContainsKey(Target)) + { + Behavior.IgnoredTargets.Add(Target, 10); + } + + isFinished = true; + return; + } + if (Target == null || Target.Removed) { isFinished = true; 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 37143041a..e3f4357c2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -15,7 +15,7 @@ namespace Barotrauma { private static readonly List> prevExplosions = new List>(); - private readonly Attack attack; + public readonly Attack Attack; private readonly float force; @@ -25,7 +25,11 @@ namespace Barotrauma private readonly float screenColorRange, screenColorDuration; private bool sparks, shockwave, flames, smoke, flash, underwaterBubble; + private bool playTinnitus; private bool applyFireEffects; + private bool ignoreCover; + private bool onlyInside; + private bool onlyOutside; private readonly float flashDuration; private readonly float? flashRange; private readonly string decal; @@ -35,14 +39,15 @@ namespace Barotrauma public float BallastFloraDamage { get; set; } - public Explosion(float range, float force, float damage, float structureDamage, float itemDamage, float empStrength = 0.0f) + public Explosion(float range, float force, float damage, float structureDamage, float itemDamage, float empStrength = 0.0f, float ballastFloraStrength = 0.0f) { - attack = new Attack(damage, 0.0f, 0.0f, structureDamage, itemDamage, range) + Attack = new Attack(damage, 0.0f, 0.0f, structureDamage, itemDamage, range) { SeverLimbsProbability = 1.0f }; this.force = force; this.EmpStrength = empStrength; + BallastFloraDamage = ballastFloraStrength; sparks = true; shockwave = true; smoke = true; @@ -52,7 +57,7 @@ namespace Barotrauma public Explosion(XElement element, string parentDebugName) { - attack = new Attack(element, parentDebugName + ", Explosion"); + Attack = new Attack(element, parentDebugName + ", Explosion"); force = element.GetAttributeFloat("force", 0.0f); @@ -62,7 +67,12 @@ namespace Barotrauma underwaterBubble = element.GetAttributeBool("underwaterbubble", true); smoke = element.GetAttributeBool("smoke", true); + playTinnitus = element.GetAttributeBool("playtinnitus", true); + applyFireEffects = element.GetAttributeBool("applyfireeffects", flames); + ignoreCover = element.GetAttributeBool("ignorecover", false); + onlyInside = element.GetAttributeBool("onlyinside", false); + onlyOutside = element.GetAttributeBool("onlyoutside", false); flash = element.GetAttributeBool("flash", true); flashDuration = element.GetAttributeFloat("flashduration", 0.05f); @@ -74,10 +84,10 @@ namespace Barotrauma decal = element.GetAttributeString("decal", ""); decalSize = element.GetAttributeFloat(1.0f, "decalSize", "decalsize"); - cameraShake = element.GetAttributeFloat("camerashake", attack.Range * 0.1f); - cameraShakeRange = element.GetAttributeFloat("camerashakerange", attack.Range); + cameraShake = element.GetAttributeFloat("camerashake", Attack.Range * 0.1f); + cameraShakeRange = element.GetAttributeFloat("camerashakerange", Attack.Range); - screenColorRange = element.GetAttributeFloat("screencolorrange", attack.Range * 0.1f); + screenColorRange = element.GetAttributeFloat("screencolorrange", Attack.Range * 0.1f); screenColor = element.GetAttributeColor("screencolor", Color.Transparent); screenColorDuration = element.GetAttributeFloat("screencolorduration", 0.1f); } @@ -113,7 +123,7 @@ namespace Barotrauma hull.AddDecal(decal, worldPosition, decalSize, isNetworkEvent: false); } - float displayRange = attack.Range; + float displayRange = Attack.Range; Vector2 cameraPos = Character.Controlled != null ? Character.Controlled.WorldPosition : GameMain.GameScreen.Cam.Position; float cameraDist = Vector2.Distance(cameraPos, worldPosition) / 2.0f; @@ -128,9 +138,9 @@ namespace Barotrauma if (displayRange < 0.1f) { return; } - if (attack.GetStructureDamage(1.0f) > 0.0f) + if (Attack.GetStructureDamage(1.0f) > 0.0f) { - RangedStructureDamage(worldPosition, displayRange, attack.GetStructureDamage(1.0f), attacker); + RangedStructureDamage(worldPosition, displayRange, Attack.GetStructureDamage(1.0f), Attack.GetLevelWallDamage(1.0f), attacker); } if (BallastFloraDamage > 0.0f) @@ -165,12 +175,12 @@ namespace Barotrauma } } - if (MathUtils.NearlyEqual(force, 0.0f) && MathUtils.NearlyEqual(attack.Stun, 0.0f) && MathUtils.NearlyEqual(attack.GetTotalDamage(false), 0.0f)) + if (MathUtils.NearlyEqual(force, 0.0f) && MathUtils.NearlyEqual(Attack.Stun, 0.0f) && MathUtils.NearlyEqual(Attack.GetTotalDamage(false), 0.0f)) { return; } - DamageCharacters(worldPosition, attack, force, damageSource, attacker); + DamageCharacters(worldPosition, Attack, force, damageSource, attacker); if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) { @@ -180,9 +190,9 @@ namespace Barotrauma float dist = Vector2.Distance(item.WorldPosition, worldPosition); float itemRadius = item.body == null ? 0.0f : item.body.GetMaxExtent(); dist = Math.Max(0.0f, dist - ConvertUnits.ToDisplayUnits(itemRadius)); - if (dist > attack.Range) { continue; } + if (dist > Attack.Range) { continue; } - if (dist < attack.Range * 0.5f && applyFireEffects && !item.FireProof) + if (dist < Attack.Range * 0.5f && applyFireEffects && !item.FireProof) { //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) @@ -209,8 +219,8 @@ namespace Barotrauma if (item.Prefab.DamagedByExplosions && !item.Indestructible) { - float distFactor = 1.0f - dist / attack.Range; - float damageAmount = attack.GetItemDamage(1.0f) * item.Prefab.ExplosionDamageMultiplier; + float distFactor = 1.0f - dist / Attack.Range; + float damageAmount = Attack.GetItemDamage(1.0f) * item.Prefab.ExplosionDamageMultiplier; Vector2 explosionPos = worldPosition; if (item.Submarine != null) { explosionPos -= item.Submarine.Position; } @@ -224,7 +234,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; } @@ -239,6 +249,8 @@ namespace Barotrauma { continue; } + if (onlyInside && c.Submarine == null) { continue; } + else if (onlyOutside && c.Submarine != null) { continue; } Vector2 explosionPos = worldPosition; if (c.Submarine != null) { explosionPos -= c.Submarine.Position; } @@ -253,7 +265,7 @@ namespace Barotrauma List modifiedAfflictions = new List(); foreach (Limb limb in c.AnimController.Limbs) { - if (limb.IsSevered || limb.IgnoreCollisions) { continue; } + if (limb.IsSevered || limb.IgnoreCollisions || !limb.body.Enabled) { continue; } float dist = Vector2.Distance(limb.WorldPosition, worldPosition); @@ -267,7 +279,10 @@ namespace Barotrauma float distFactor = 1.0f - dist / attack.Range; //solid obstacles between the explosion and the limb reduce the effect of the explosion - distFactor *= GetObstacleDamageMultiplier(explosionPos, worldPosition, limb.SimPosition); + if (!ignoreCover) + { + distFactor *= GetObstacleDamageMultiplier(explosionPos, worldPosition, limb.SimPosition); + } distFactors.Add(limb, distFactor); modifiedAfflictions.Clear(); @@ -322,10 +337,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); } @@ -354,7 +369,7 @@ namespace Barotrauma /// /// Returns a dictionary where the keys are the structures that took damage and the values are the amount of damage taken /// - public static Dictionary RangedStructureDamage(Vector2 worldPosition, float worldRange, float damage, Character attacker = null, bool damageLevelWalls = true) + public static Dictionary RangedStructureDamage(Vector2 worldPosition, float worldRange, float damage, float levelWallDamage, Character attacker = null) { List structureList = new List(); float dist = 600.0f; @@ -391,7 +406,7 @@ namespace Barotrauma } } - if (Level.Loaded != null && damageLevelWalls) + if (Level.Loaded != null && !MathUtils.NearlyEqual(levelWallDamage, 0.0f)) { for (int i = Level.Loaded.ExtraWalls.Count - 1; i >= 0; i--) { @@ -400,7 +415,7 @@ namespace Barotrauma { if (cell.IsPointInside(worldPosition)) { - destructibleWall.AddDamage(damage, worldPosition); + destructibleWall.AddDamage(levelWallDamage, worldPosition); continue; } foreach (var edge in cell.Edges) 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..8b3803f99 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; @@ -934,14 +939,14 @@ namespace Barotrauma /// Approximate distance from this hull to the target hull, moving through open gaps without passing through walls. /// Uses a greedy algo and may not use the most optimal path. Returns float.MaxValue if no path is found. /// - public float GetApproximateDistance(Vector2 startPos, Vector2 endPos, Hull targetHull, float maxDistance) + public float GetApproximateDistance(Vector2 startPos, Vector2 endPos, Hull targetHull, float maxDistance, float distanceMultiplierPerClosedDoor = 0) { - return GetApproximateHullDistance(startPos, endPos, new HashSet(), targetHull, 0.0f, maxDistance); + return GetApproximateHullDistance(startPos, endPos, new HashSet(), targetHull, 0.0f, maxDistance, distanceMultiplierPerClosedDoor); } - private float GetApproximateHullDistance(Vector2 startPos, Vector2 endPos, HashSet connectedHulls, Hull target, float distance, float maxDistance) + private float GetApproximateHullDistance(Vector2 startPos, Vector2 endPos, HashSet connectedHulls, Hull target, float distance, float maxDistance, float distanceMultiplierFromDoors = 0) { - if (distance >= maxDistance) return float.MaxValue; + if (distance >= maxDistance) { return float.MaxValue; } if (this == target) { return distance + Vector2.Distance(startPos, endPos); @@ -951,12 +956,17 @@ namespace Barotrauma foreach (Gap g in ConnectedGaps) { + float distanceMultiplier = 1; if (g.ConnectedDoor != null && !g.ConnectedDoor.IsBroken) { //gap blocked if the door is not open or the predicted state is not open if ((!g.ConnectedDoor.IsOpen && !g.ConnectedDoor.IsBroken) || (g.ConnectedDoor.PredictedState.HasValue && !g.ConnectedDoor.PredictedState.Value)) { - if (g.ConnectedDoor.OpenState < 0.1f) continue; + if (g.ConnectedDoor.OpenState < 0.1f) + { + if (distanceMultiplierFromDoors <= 0) { continue; } + distanceMultiplier *= distanceMultiplierFromDoors; + } } } else if (g.Open <= 0.0f) @@ -968,8 +978,11 @@ namespace Barotrauma { if (g.linkedTo[i] is Hull hull && !connectedHulls.Contains(hull)) { - float dist = hull.GetApproximateHullDistance(g.Position, endPos, connectedHulls, target, distance + Vector2.Distance(startPos, g.Position), maxDistance); - if (dist < float.MaxValue) { return dist; } + float dist = hull.GetApproximateHullDistance(g.Position, endPos, connectedHulls, target, distance + Vector2.Distance(startPos, g.Position) * distanceMultiplier, maxDistance); + if (dist < float.MaxValue) + { + return dist; + } } } } 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..8ce97ea4d 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,23 +123,30 @@ 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) + { + return PasteEntities(position, sub, configElement, FilePath, selectInstance); + } + + public static List PasteEntities(Vector2 position, Submarine sub, XElement configElement, string filePath = null, bool selectInstance = false) { int idOffset = Entity.FindFreeID(1); if (MapEntity.mapEntityList.Any()) { idOffset = MapEntity.mapEntityList.Max(e => e.ID); } - List entities = MapEntity.LoadAll(sub, configElement, FilePath, idOffset); + List entities = MapEntity.LoadAll(sub, configElement, filePath, idOffset); if (entities.Count == 0) { return entities; } - Vector2 offset = sub == null ? Vector2.Zero : sub.HiddenSubPosition; + Vector2 offset = sub?.HiddenSubPosition ?? Vector2.Zero; foreach (MapEntity me in entities) { @@ -148,14 +168,13 @@ 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); } -#endif +#endif return entities; - } public void Delete() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerationParams.cs index 2bf8a1b1b..e365e2c66 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; @@ -100,19 +101,19 @@ namespace Barotrauma public Sprite WallSprite { get; private set; } public Sprite WallEdgeSprite { get; private set; } - public static CaveGenerationParams GetRandom(LevelGenerationParams generationParams, Rand.RandSync rand) + public static CaveGenerationParams GetRandom(LevelGenerationParams generationParams, bool abyss, Rand.RandSync rand) { - if (CaveParams.All(p => p.GetCommonness(generationParams) <= 0.0f)) + if (CaveParams.All(p => p.GetCommonness(generationParams, abyss) <= 0.0f)) { return CaveParams.First(); } - return ToolBox.SelectWeightedRandom(CaveParams, CaveParams.Select(p => p.GetCommonness(generationParams)).ToList(), rand); + return ToolBox.SelectWeightedRandom(CaveParams, CaveParams.Select(p => p.GetCommonness(generationParams, abyss)).ToList(), rand); } - public float GetCommonness(LevelGenerationParams generationParams) + public float GetCommonness(LevelGenerationParams generationParams, bool abyss) { if (generationParams?.Identifier != null && - OverrideCommonness.TryGetValue(generationParams.Identifier, out float commonness)) + OverrideCommonness.TryGetValue(abyss ? "abyss" : generationParams.Identifier, out float commonness)) { return commonness; } @@ -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 8ccd3ca54..3eb478afb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs @@ -141,69 +141,36 @@ namespace Barotrauma return cells; } - - private static Vector2 GetEdgeNormal(GraphEdge edge, VoronoiCell cell = null) - { - if (cell == null) { cell = edge.AdjacentCell(null); } - if (cell == null) { return Vector2.UnitX; } - - CompareCCW compare = new CompareCCW(cell.Center); - if (compare.Compare(edge.Point1, edge.Point2) == -1) - { - var temp = edge.Point1; - edge.Point1 = edge.Point2; - edge.Point2 = temp; - } - - Vector2 normal = Vector2.Normalize(edge.Point2 - edge.Point1); - Vector2 diffToCell = Vector2.Normalize(cell.Center - edge.Point2); - - normal = new Vector2(-normal.Y, normal.X); - if (Vector2.Dot(normal, diffToCell) < 0) - { - normal = -normal; - } - - return normal; - } - - public static void GeneratePath(Level.Tunnel tunnel, List cells, List[,] cellGrid, int gridCellSize, Rectangle limits) + public static void GeneratePath(Level.Tunnel tunnel, Level level) { var targetCells = new List(); for (int i = 0; i < tunnel.Nodes.Count; i++) { - //a search depth of 2 is large enough to find a cell in almost all maps, but in case it fails, we increase the depth - int searchDepth = 2; - while (searchDepth < 5) + var closestCell = level.GetClosestCell(tunnel.Nodes[i].ToVector2()); + if (closestCell != null && !targetCells.Contains(closestCell)) { - int cellIndex = FindCellIndex(tunnel.Nodes[i], cells, cellGrid, gridCellSize, searchDepth); - if (cellIndex > -1) - { - targetCells.Add(cells[cellIndex]); - break; - } - - searchDepth++; + targetCells.Add(closestCell); } } - tunnel.Cells.AddRange(GeneratePath(targetCells, cells, limits)); + tunnel.Cells.AddRange(GeneratePath(targetCells, level.GetAllCells())); } - - public static List GeneratePath(List targetCells, List cells, Rectangle limits) + public static List GeneratePath(List targetCells, List cells) { Stopwatch sw2 = new Stopwatch(); 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 { @@ -216,10 +183,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) { @@ -258,7 +229,7 @@ namespace Barotrauma List tempEdges = new List(); foreach (GraphEdge edge in cell.Edges) { - if (!edge.IsSolid) + if (!edge.IsSolid || edge.OutsideLevel) { tempEdges.Add(edge); continue; @@ -289,7 +260,8 @@ namespace Barotrauma } List edgePoints = new List(); - Vector2 edgeNormal = GetEdgeNormal(edge, cell); + Vector2 edgeNormal = edge.GetNormal(cell); + float edgeLength = Vector2.Distance(edge.Point1, edge.Point2); int pointCount = (int)Math.Max(Math.Ceiling(edgeLength / minEdgeLength), 1); Vector2 edgeDir = edge.Point2 - edge.Point1; @@ -310,12 +282,39 @@ namespace Barotrauma float randomVariance = Rand.Range(0, irregularity, Rand.RandSync.Server); Vector2 extrudedPoint = edge.Point1 + - edgeDir * (i / (float)pointCount) - + edgeDir * (i / (float)pointCount) + edgeNormal * edgeLength * (roundingAmount + randomVariance) * centerF; - //check if extruding the edge causes it to go inside another one - var nearbyCells = Level.Loaded.GetCells(extrudedPoint, searchDepth: 1); - if (!nearbyCells.Any(c => c.CellType == CellType.Solid && c != cell && c.IsPointInside(extrudedPoint))) { edgePoints.Add(extrudedPoint); } + var nearbyCells = Level.Loaded.GetCells(extrudedPoint, searchDepth: 2); + bool isInside = false; + foreach (var nearbyCell in nearbyCells) + { + if (nearbyCell == cell || nearbyCell.CellType != CellType.Solid) { continue; } + //check if extruding the edge causes it to go inside another one + if (nearbyCell.IsPointInside(extrudedPoint)) + { + isInside = true; + break; + } + //check if another edge will be inside this cell after the extrusion + Vector2 triangleCenter = (edge.Point1 + edge.Point2 + extrudedPoint) / 3; + foreach (GraphEdge nearbyEdge in nearbyCell.Edges) + { + if (!MathUtils.LinesIntersect(nearbyEdge.Point1, triangleCenter, edge.Point1, extrudedPoint) && + !MathUtils.LinesIntersect(nearbyEdge.Point1, triangleCenter, edge.Point2, extrudedPoint) && + !MathUtils.LinesIntersect(nearbyEdge.Point1, triangleCenter, edge.Point1, edge.Point2)) + { + isInside = true; + break; + } + } + if (isInside) { break; } + } + + if (!isInside) + { + edgePoints.Add(extrudedPoint); + } } } @@ -381,7 +380,19 @@ namespace Barotrauma continue; } - renderTriangles.AddRange(MathUtils.TriangulateConvexHull(tempVertices, cell.Center)); + Vector2 minVert = tempVertices[0]; + Vector2 maxVert = tempVertices[0]; + foreach (var vert in tempVertices) + { + minVert = new Vector2( + Math.Min(minVert.X, vert.X), + Math.Min(minVert.Y, vert.Y)); + maxVert = new Vector2( + Math.Max(maxVert.X, vert.X), + Math.Max(maxVert.Y, vert.Y)); + } + Vector2 center = (minVert + maxVert) / 2; + renderTriangles.AddRange(MathUtils.TriangulateConvexHull(tempVertices, center)); if (bodyPoints.Count < 2) { continue; } @@ -404,7 +415,7 @@ namespace Barotrauma if (cell.CellType == CellType.Empty) { continue; } cellBody.UserData = cell; - var triangles = MathUtils.TriangulateConvexHull(bodyPoints, ConvertUnits.ToSimUnits(cell.Center)); + var triangles = MathUtils.TriangulateConvexHull(bodyPoints, ConvertUnits.ToSimUnits(center)); for (int i = 0; i < triangles.Count; i++) { @@ -435,14 +446,21 @@ namespace Barotrauma } cell.Body = cellBody; } + + cellBody.CollisionCategories = Physics.CollisionLevel; cellBody.ResetMassData(); return cellBody; } - + public static List CreateRandomChunk(float radius, int vertexCount, float radiusVariance) { - Debug.Assert(radiusVariance < radius); + return CreateRandomChunk(radius * 2, radius * 2, vertexCount, radiusVariance); + } + + public static List CreateRandomChunk(float width, float height, int vertexCount, float radiusVariance) + { + Debug.Assert(radiusVariance < Math.Min(width, height)); Debug.Assert(vertexCount >= 3); List verts = new List(); @@ -450,72 +468,12 @@ namespace Barotrauma float angle = 0.0f; for (int i = 0; i < vertexCount; i++) { - verts.Add(new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle)) * - (radius + Rand.Range(-radiusVariance, radiusVariance, Rand.RandSync.Server))); + Vector2 dir = new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle)); + verts.Add(new Vector2(dir.X * width / 2, dir.Y * height / 2) + dir * Rand.Range(-radiusVariance, radiusVariance, Rand.RandSync.Server)); angle += angleStep; } return verts; } - /// - /// find the index of the cell which the point is inside - /// (actually finds the cell whose center is closest, but it's always the correct cell assuming the point is inside the borders of the diagram) - /// - public static int FindCellIndex(Vector2 position,List cells, List[,] cellGrid, int gridCellSize, int searchDepth = 1, Vector2? offset = null) - { - float closestDist = float.PositiveInfinity; - VoronoiCell closestCell = null; - - Vector2 gridOffset = offset == null ? Vector2.Zero : (Vector2)offset; - position -= gridOffset; - - int gridPosX = (int)Math.Floor(position.X / gridCellSize); - int gridPosY = (int)Math.Floor(position.Y / gridCellSize); - - for (int x = Math.Max(gridPosX - searchDepth, 0); x <= Math.Min(gridPosX + searchDepth, cellGrid.GetLength(0) - 1); x++) - { - for (int y = Math.Max(gridPosY - searchDepth, 0); y <= Math.Min(gridPosY + searchDepth, cellGrid.GetLength(1) - 1); y++) - { - for (int i = 0; i < cellGrid[x, y].Count; i++) - { - float dist = Vector2.DistanceSquared(cellGrid[x, y][i].Center, position); - if (dist > closestDist) continue; - - closestDist = dist; - closestCell = cellGrid[x, y][i]; - } - } - } - - return cells.IndexOf(closestCell); - } - - public static int FindCellIndex(Point position, List cells, List[,] cellGrid, int gridCellSize, int searchDepth = 1) - { - int closestDist = int.MaxValue; - VoronoiCell closestCell = null; - - int gridPosX = position.X / gridCellSize; - int gridPosY = position.Y / gridCellSize; - - for (int x = Math.Max(gridPosX - searchDepth, 0); x <= Math.Min(gridPosX + searchDepth, cellGrid.GetLength(0) - 1); x++) - { - for (int y = Math.Max(gridPosY - searchDepth, 0); y <= Math.Min(gridPosY + searchDepth, cellGrid.GetLength(1) - 1); y++) - { - for (int i = 0; i < cellGrid[x, y].Count; i++) - { - int dist = MathUtils.DistanceSquared( - (int)cellGrid[x, y][i].Site.Coord.X, (int)cellGrid[x, y][i].Site.Coord.Y, - position.X, position.Y); - if (dist > closestDist) continue; - - closestDist = dist; - closestCell = cellGrid[x, y][i]; - } - } - } - - return cells.IndexOf(closestCell); - } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/DestructibleLevelWall.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/DestructibleLevelWall.cs index e681c8057..a9fb2d699 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/DestructibleLevelWall.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/DestructibleLevelWall.cs @@ -58,7 +58,8 @@ 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); } public override void Update(float deltaTime) @@ -201,6 +202,10 @@ namespace Barotrauma if (Destroyed) { return; } Destroyed = true; level?.UnsyncedExtraWalls?.Remove(this); + foreach (var cell in Cells) + { + cell.CellType = CellType.Removed; + } GameMain.World.Remove(Body); Dispose(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 863809161..2c9962767 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -35,7 +35,9 @@ namespace Barotrauma Cave = 0x4, Ruin = 0x8, Wreck = 0x10, - BeaconStation = 0x20 + BeaconStation = 0x20, + Abyss = 0x40, + AbyssCave = 0x80 } public struct InterestingPosition @@ -45,14 +47,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 +129,8 @@ namespace Barotrauma public Point StartPos, EndPos; + public bool DisplayOnSonar; + public readonly CaveGenerationParams CaveGenerationParams; public Cave(CaveGenerationParams caveGenerationParams, Rectangle area, Point startPos, Point endPos) @@ -123,11 +148,36 @@ namespace Barotrauma private List[,] cellGrid; private List cells; + public Rectangle AbyssArea + { + get; + private set; + } + + public int AbyssStart + { + get { return AbyssArea.Y + AbyssArea.Height; } + } + + public class AbyssIsland + { + public readonly Rectangle Area; + public readonly List Cells; + + public AbyssIsland(Rectangle area, List cells) + { + Debug.Assert(cells != null && cells.Any()); + Area = area; + Cells = cells; + } + } + public List AbyssIslands = new List(); + //TODO: make private public List siteCoordsX, siteCoordsY; //TODO: make private - public List> distanceField; + public List<(Point point, double distance)> distanceField; private Point startPosition, endPosition; @@ -328,8 +378,11 @@ namespace Barotrauma EntitiesBeforeGenerate = GetEntities().ToList(); EntityCountBeforeGenerate = EntitiesBeforeGenerate.Count(); - StartLocation = GameMain.GameSession?.StartLocation; - EndLocation = GameMain.GameSession?.EndLocation; + if (LevelData.ForceOutpostGenerationParams == null) + { + StartLocation = GameMain.GameSession?.StartLocation; + EndLocation = GameMain.GameSession?.EndLocation; + } EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); @@ -375,6 +428,7 @@ namespace Barotrauma minWidth = Math.Min(minWidth, MaxSubmarineWidth); } minWidth = Math.Min(minWidth, borders.Width / 5); + LevelData.MinMainPathWidth = minWidth; Rectangle pathBorders = borders; pathBorders.Inflate( @@ -421,7 +475,8 @@ namespace Barotrauma } CalculateTunnelDistanceField(density: 1000); - GenerateSeaFloorPositions(mirror); + GenerateSeaFloorPositions(); + GenerateAbyssArea(); GenerateCaves(mainPath); EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); @@ -435,6 +490,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 +504,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 +515,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,28 +530,33 @@ 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 + siteInterval.X / 2); - siteCoordsY.Add(y); - } - if (y < borders.Height - siteInterval.Y) - { - siteCoordsX.Add(x); - siteCoordsY.Add(y + siteInterval.Y / 2); - } - if (x < borders.Width - siteInterval.X && y < borders.Height - siteInterval.Y) - { - siteCoordsX.Add(x + siteInterval.X / 2); - siteCoordsY.Add(y + siteInterval.Y / 2); - } - } - 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); + } + } + } } } @@ -516,7 +577,10 @@ namespace Barotrauma //construct voronoi cells based on the graph edges cells = CaveGenerator.GraphEdgesToCells(graphEdges, borders, GridCellSize, out cellGrid); - + + GenerateAbyssGeometry(); + GenerateAbyssPositions(); + Debug.WriteLine("find cells: " + sw2.ElapsedMilliseconds + " ms"); sw2.Restart(); @@ -527,14 +591,16 @@ namespace Barotrauma List pathCells = new List(); foreach (Tunnel tunnel in Tunnels) { - CaveGenerator.GeneratePath(tunnel, cells, cellGrid, GridCellSize, pathBorders); + CaveGenerator.GeneratePath(tunnel, this); if (tunnel.Type == TunnelType.MainPath || tunnel.Type == TunnelType.SidePath) { - for (int i = 2; i < tunnel.Cells.Count; i += 3) + var distinctCells = tunnel.Cells.Distinct().ToList(); + for (int i = 2; i < distinctCells.Count; i += 3) { PositionsOfInterest.Add(new InterestingPosition( - new Point((int)tunnel.Cells[i].Site.Coord.X, (int)tunnel.Cells[i].Site.Coord.Y), - tunnel.Type == TunnelType.MainPath ? PositionType.MainPath : PositionType.SidePath)); + new Point((int)distinctCells[i].Site.Coord.X, (int)distinctCells[i].Site.Coord.Y), + tunnel.Type == TunnelType.MainPath ? PositionType.MainPath : PositionType.SidePath, + Caves.Find(cave => cave.Tunnels.Contains(tunnel)))); } } GenerateWaypoints(tunnel, parentTunnel: tunnel.ParentTunnel); @@ -610,7 +676,14 @@ namespace Barotrauma cells = cells.Except(pathCells).ToList(); //remove cells from the edges and bottom of the map because a clean-cut edge of the level looks bad - cells.RemoveAll(c => c.Edges.Any(e => !MathUtils.NearlyEqual(e.Point1.Y, Size.Y) && e.AdjacentCell(c) == null)); + cells.ForEachMod(c => + { + if (c.Edges.Any(e => !MathUtils.NearlyEqual(e.Point1.Y, Size.Y) && e.AdjacentCell(c) == null)) + { + c.CellType = CellType.Removed; + cells.Remove(c); + } + }); int xPadding = borders.Width / 5; pathCells.AddRange(CreateHoles(GenerationParams.BottomHoleProbability, new Rectangle(xPadding, 0, borders.Width - xPadding * 2, Size.Y / 2), minWidth)); @@ -621,14 +694,18 @@ namespace Barotrauma cell.Edges.ForEach(e => e.OutsideLevel = true); } + foreach (AbyssIsland abyssIsland in AbyssIslands) + { + cells.AddRange(abyssIsland.Cells); + } + //---------------------------------------------------------------------------------- // initialize the cells that are still left and insert them into the cell grid //---------------------------------------------------------------------------------- - + foreach (VoronoiCell cell in pathCells) { cell.Edges.ForEach(e => e.OutsideLevel = false); - cell.CellType = CellType.Path; cells.Remove(cell); } @@ -696,7 +773,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) @@ -705,29 +787,41 @@ namespace Barotrauma waypoint.Move(new Vector2((borders.Width / 2 - waypoint.Position.X) * 2, 0.0f)); } + for (int i = 0; i < bottomPositions.Count; i++) + { + bottomPositions[i] = new Point(borders.Size.X - bottomPositions[i].X, bottomPositions[i].Y); + } + bottomPositions.Reverse(); + startPosition.X = borders.Width - startPosition.X; endPosition.X = borders.Width - endPosition.X; + + CalculateTunnelDistanceField(density: 1000); } foreach (VoronoiCell cell in cells) { int x = (int)Math.Floor(cell.Site.Coord.X / GridCellSize); + x = MathHelper.Clamp(x, 0, cellGrid.GetLength(0) - 1); int y = (int)Math.Floor(cell.Site.Coord.Y / GridCellSize); - - if (x < 0 || y < 0 || x >= cellGrid.GetLength(0) || y >= cellGrid.GetLength(1)) { continue; } + y = MathHelper.Clamp(y, 0, cellGrid.GetLength(1) - 1); 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); + if (cave.Area.Y > 0) + { + CreatePathToClosestTunnel(cave.StartPos); + } List caveCells = new List(); 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) @@ -748,7 +842,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)); @@ -812,38 +906,46 @@ namespace Barotrauma #if CLIENT - List, Cave>> cellBatches = new List, Cave>> + List<(List cells, Cave parentCave)> cellBatches = new List<(List, Cave)> { - new Pair, Cave>(cellsWithBody.ToList(), null) + (cellsWithBody.ToList(), null) }; foreach (Cave cave in Caves) { - cellBatches.Add(new Pair, Cave>(new List(), cave)); + (List cells, Cave parentCave) newCellBatch = (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.cells.Contains(edge.Cell1)) { - cellBatches.First().First.Remove(edge.Cell1); - cellBatches.Last().First.Add(edge.Cell1); + Debug.Assert(cellsWithBody.Contains(edge.Cell1)); + cellBatches.ForEach(cb => cb.cells.Remove(edge.Cell1)); + newCellBatch.cells.Add(edge.Cell1); } - if (edge.Cell2?.CellType == CellType.Solid && !cellBatches.Last().First.Contains(edge.Cell2)) + if (edge.Cell2?.CellType == CellType.Solid && !newCellBatch.cells.Contains(edge.Cell2)) { - cellBatches.First().First.Remove(edge.Cell2); - cellBatches.Last().First.Add(edge.Cell2); + Debug.Assert(cellsWithBody.Contains(edge.Cell2)); + cellBatches.ForEach(cb => cb.cells.Remove(edge.Cell2)); + newCellBatch.cells.Add(edge.Cell2); } } } + if (newCellBatch.cells.Any()) + { + cellBatches.Add(newCellBatch); + } } + cellBatches.RemoveAll(cb => !cb.cells.Any()); - Debug.Assert(cellsWithBody.Count == cellBatches.Sum(cb => cb.First.Count)); + int totalCellsInBatches = cellBatches.Sum(cb => cb.cells.Count); + Debug.Assert(cellsWithBody.Count == totalCellsInBatches); List> triangleLists = new List>(); - foreach (Pair, Cave> cellBatch in cellBatches) + foreach ((List cells, Cave cave) cellBatch in cellBatches) { - bodies.Add(CaveGenerator.GeneratePolygons(cellBatch.First, this, out List triangles)); + bodies.Add(CaveGenerator.GeneratePolygons(cellBatch.cells, this, out List triangles)); triangleLists.Add(triangles); } #else @@ -874,9 +976,9 @@ namespace Barotrauma { renderer.SetVertices( CaveGenerator.GenerateWallVertices(triangleLists[i], GenerationParams, zCoord: 0.9f).ToArray(), - CaveGenerator.GenerateWallEdgeVertices(cellBatches[i].First, this, zCoord: 0.9f).ToArray(), - cellBatches[i].Second?.CaveGenerationParams?.WallSprite == null ? GenerationParams.WallSprite.Texture : cellBatches[i].Second.CaveGenerationParams.WallSprite.Texture, - cellBatches[i].Second?.CaveGenerationParams?.WallEdgeSprite == null ? GenerationParams.WallEdgeSprite.Texture : cellBatches[i].Second.CaveGenerationParams.WallEdgeSprite.Texture, + CaveGenerator.GenerateWallEdgeVertices(cellBatches[i].cells, this, zCoord: 0.9f).ToArray(), + cellBatches[i].parentCave?.CaveGenerationParams?.WallSprite == null ? GenerationParams.WallSprite.Texture : cellBatches[i].parentCave.CaveGenerationParams.WallSprite.Texture, + cellBatches[i].parentCave?.CaveGenerationParams?.WallEdgeSprite == null ? GenerationParams.WallEdgeSprite.Texture : cellBatches[i].parentCave.CaveGenerationParams.WallEdgeSprite.Texture, GenerationParams.WallColor); } #endif @@ -1133,13 +1235,17 @@ 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; - var newWaypoint = new WayPoint(new Rectangle((int)tunnel.Cells[i].Site.Coord.X, (int)tunnel.Cells[i].Center.Y, 10, 10), null); + var newWaypoint = new WayPoint(new Rectangle((int)tunnel.Cells[i].Site.Coord.X, (int)tunnel.Cells[i].Center.Y, 10, 10), null) + { + Tunnel = tunnel + }; wayPoints.Add(newWaypoint); if (wayPoints.Count > 1) @@ -1218,10 +1324,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)) { @@ -1231,25 +1335,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) { @@ -1257,18 +1349,186 @@ namespace Barotrauma foreach (GraphEdge edge in cell.Edges) { if (Vector2.DistanceSquared(edge.Point1, position) < minDistSqr || - Vector2.DistanceSquared(edge.Point2, position) < minDistSqr) + Vector2.DistanceSquared(edge.Point2, position) < minDistSqr || + MathUtils.LineSegmentToPointDistanceSquared(edge.Point1.ToPoint(), edge.Point2.ToPoint(), position.ToPoint()) < minDistSqr) { tooClose = true; break; } } if (tooClose) { tooCloseCells.Add(cell); } - } + } return tooCloseCells.ToList(); } - private void GenerateSeaFloorPositions(bool mirror) + private void GenerateAbyssPositions() + { + int count = 10; + for (int i = 0; i < count; i++) + { + float xPos = MathHelper.Lerp(borders.X, borders.Right, i / (float)(count - 1)); + float seaFloorPos = GetBottomPosition(xPos).Y; + + //above the bottom of the level = can't place a point here + if (seaFloorPos > AbyssArea.Bottom) { continue; } + + float yPos = Rand.Range(Math.Max(seaFloorPos, AbyssArea.Y), AbyssArea.Bottom); + + foreach (var abyssIsland in AbyssIslands) + { + if (abyssIsland.Area.Contains(new Point((int)xPos, (int)yPos))) + { + xPos = abyssIsland.Area.Center.X + (int)(Rand.Int(1) == 0 ? abyssIsland.Area.Width * -0.6f : 0.6f); + } + } + + PositionsOfInterest.Add(new InterestingPosition(new Point((int)xPos, (int)yPos), PositionType.Abyss)); + } + } + + private void GenerateAbyssArea() + { + int abyssStartY = borders.Y - 5000; + int abyssEndY = Math.Max(abyssStartY - 100000, BottomPos + 1000); + int abyssHeight = abyssStartY - abyssEndY; + + if (abyssHeight < 0) + { + abyssStartY = borders.Y; + abyssEndY = BottomPos; + if (abyssStartY - abyssEndY < 1000) + { +#if DEBUG + DebugConsole.ThrowError("Not enough space to generate Abyss in the level. You may want to move the ocean floor deeper."); +#else + DebugConsole.AddWarning("Not enough space to generate Abyss in the level. You may want to move the ocean floor deeper."); +#endif + } + } + else + { + //if the bottom of the abyss area is below crush depth, try to move it up to keep (most) of the abyss content above crush depth + if (abyssEndY + CrushDepth < 0) + { + abyssEndY += Math.Min(-(abyssEndY + (int)CrushDepth), abyssHeight / 2); + } + + if (abyssStartY - abyssEndY < 10000) + { + abyssStartY = borders.Y; + } + } + + AbyssArea = new Rectangle(borders.X, abyssEndY, borders.Width, abyssStartY - abyssEndY); + } + + private void GenerateAbyssGeometry() + { + //TODO: expose island parameters + + Voronoi voronoi = new Voronoi(1.0); + Point siteInterval = new Point(500, 500); + Point siteVariance = new Point(200, 200); + + Point islandSize = Vector2.Lerp( + GenerationParams.AbyssIslandSizeMin.ToVector2(), + GenerationParams.AbyssIslandSizeMax.ToVector2(), + Rand.Range(0.0f, 1.0f, Rand.RandSync.Server)).ToPoint(); + + if (AbyssArea.Height < islandSize.Y) { return; } + + int islandCount = GenerationParams.AbyssIslandCount; + for (int i = 0; i < islandCount; i++) + { + Point islandPosition = Point.Zero; + Rectangle islandArea = new Rectangle(islandPosition, islandSize); + + //prevent overlaps + int tries = 0; + const int MaxTries = 20; + do + { + islandPosition = new Point( + Rand.Range(AbyssArea.X, AbyssArea.Right - islandSize.X, Rand.RandSync.Server), + Rand.Range(AbyssArea.Y, AbyssArea.Bottom - islandSize.Y, Rand.RandSync.Server)); + + //move the island above the sea floor geometry + islandPosition.Y = Math.Max(islandPosition.Y, (int)GetBottomPosition(islandPosition.X).Y + 500); + islandPosition.Y = Math.Max(islandPosition.Y, (int)GetBottomPosition(islandPosition.X + islandArea.Width).Y + 500); + + islandArea.Location = islandPosition; + + tries++; + } while ((AbyssIslands.Any(island => island.Area.Intersects(islandArea)) || islandArea.Bottom > AbyssArea.Bottom) && tries < MaxTries); + + if (tries >= MaxTries) + { + break; + } + + if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) > GenerationParams.AbyssIslandCaveProbability) + { + float radiusVariance = Math.Min(islandArea.Width, islandArea.Height) * 0.1f; + var vertices = CaveGenerator.CreateRandomChunk(islandArea.Width - (int)(radiusVariance * 2), islandArea.Height - (int)(radiusVariance * 2), 16, radiusVariance: radiusVariance); + Vector2 position = islandArea.Center.ToVector2(); + for (int j = 0; j < vertices.Count; j++) + { + vertices[j] += position; + } + var newChunk = new LevelWall(vertices, GenerationParams.WallColor, this); + AbyssIslands.Add(new AbyssIsland(islandArea, newChunk.Cells)); + continue; + } + + var siteCoordsX = new List((islandSize.Y / siteInterval.Y) * (islandSize.X / siteInterval.Y)); + var siteCoordsY = new List((islandSize.Y / siteInterval.Y) * (islandSize.X / siteInterval.Y)); + + for (int x = islandArea.X; x < islandArea.Right; x += siteInterval.X) + { + for (int y = islandArea.Y; y < islandArea.Bottom; y += siteInterval.Y) + { + siteCoordsX.Add(x + Rand.Range(-siteVariance.X, siteVariance.X, Rand.RandSync.Server)); + siteCoordsY.Add(y + Rand.Range(-siteVariance.Y, siteVariance.Y, Rand.RandSync.Server)); + } + } + + var graphEdges = voronoi.MakeVoronoiGraph(siteCoordsX.ToArray(), siteCoordsY.ToArray(), islandArea); + var islandCells = CaveGenerator.GraphEdgesToCells(graphEdges, islandArea, GridCellSize, out var cellGrid); + + //make the island elliptical + for (int j = islandCells.Count - 1; j >= 0; j--) + { + var cell = islandCells[j]; + double xDiff = (cell.Site.Coord.X - islandArea.Center.X) / (islandArea.Width * 0.5); + double yDiff = (cell.Site.Coord.Y - islandArea.Center.Y) / (islandArea.Height * 0.5); + + //a conical stalactite-like shape at the bottom + if (yDiff < 0) { xDiff += xDiff * Math.Abs(yDiff); } + + double normalizedDist = Math.Sqrt(xDiff * xDiff + yDiff * yDiff); + if (normalizedDist > 0.95 || + cell.Edges.Any(e => MathUtils.NearlyEqual(e.Point1.X, islandArea.X)) || + cell.Edges.Any(e => MathUtils.NearlyEqual(e.Point1.X, islandArea.Right)) || + cell.Edges.Any(e => MathUtils.NearlyEqual(e.Point1.Y, islandArea.Y)) || + cell.Edges.Any(e => MathUtils.NearlyEqual(e.Point1.Y, islandArea.Bottom))) + { + islandCells[j].CellType = CellType.Removed; + islandCells.RemoveAt(j); + } + } + + var caveParams = CaveGenerationParams.GetRandom(GenerationParams, abyss: true, rand: Rand.RandSync.Server); + + float caveScaleRelativeToIsland = 0.7f; + GenerateCave( + caveParams, Tunnels.First(), + new Point(islandArea.Center.X, islandArea.Center.Y + (int)(islandArea.Size.Y * (1.0f - caveScaleRelativeToIsland)) / 2), + new Point((int)(islandArea.Size.X * caveScaleRelativeToIsland), (int)(islandArea.Size.Y * caveScaleRelativeToIsland))); + AbyssIslands.Add(new AbyssIsland(islandArea, islandCells)); + } + } + + private void GenerateSeaFloorPositions() { BottomPos = GenerationParams.SeaFloorDepth; SeaFloorTopPos = BottomPos; @@ -1303,14 +1563,6 @@ namespace Barotrauma currInverval /= 2; } - if (mirror) - { - for (int i = 0; i < bottomPositions.Count; i++) - { - bottomPositions[i] = new Point(borders.Size.X - bottomPositions[i].X, bottomPositions[i].Y); - } - } - SeaFloorTopPos = bottomPositions.Max(p => p.Y); } @@ -1334,88 +1586,101 @@ namespace Barotrauma { for (int i = 0; i < GenerationParams.CaveCount; i++) { - var caveParams = CaveGenerationParams.GetRandom(GenerationParams, Rand.RandSync.Server); + var caveParams = CaveGenerationParams.GetRandom(GenerationParams, abyss: false, rand: Rand.RandSync.Server); Point caveSize = new Point( Rand.Range(caveParams.MinWidth, caveParams.MaxWidth, Rand.RandSync.Server), Rand.Range(caveParams.MinHeight, caveParams.MaxHeight, Rand.RandSync.Server)); - int radius = Math.Max(caveSize.X, caveSize.Y) / 2; 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); + int radius = Math.Max(caveSize.X, caveSize.Y) / 2; + var cavePos = FindPosAwayFromMainPath((parentTunnel.MinWidth + radius) * 1.2f, asCloseAsPossible: true, allowedArea); - Point closestParentNode = parentTunnel.Nodes.First(); - double closestDist = double.PositiveInfinity; - foreach (Point node in parentTunnel.Nodes) - { - double dist = MathUtils.DistanceSquared((double)node.X, (double)node.Y, (double)cavePos.X, (double)cavePos.Y); - if (dist < closestDist) - { - closestParentNode = node; - closestDist = dist; - } - } - - Rectangle caveArea = new Rectangle(cavePos - new Point(caveSize.X / 2, caveSize.Y / 2), caveSize); - MathUtils.GetLineRectangleIntersection(closestParentNode.ToVector2(), cavePos.ToVector2(), new Rectangle(caveArea.X, caveArea.Y + caveArea.Height, caveArea.Width, caveArea.Height), out Vector2 caveStartPosVector); - - Point caveStartPos = caveStartPosVector.ToPoint(); - Point caveEndPos = cavePos - (caveStartPos - cavePos); - - Cave cave = new Cave(caveParams, caveArea, caveStartPos, caveEndPos); - Caves.Add(cave); - - var caveSegments = MathUtils.GenerateJaggedLine( - caveStartPos.ToVector2(), caveEndPos.ToVector2(), - iterations: 3, - offsetAmount: Vector2.Distance(caveStartPos.ToVector2(), caveEndPos.ToVector2()) * 0.75f); - if (!caveSegments.Any()) { continue; } - - List caveBranches = new List(); - - var tunnel = new Tunnel(TunnelType.Cave, SegmentsToNodes(caveSegments), 100, parentTunnel); - Tunnels.Add(tunnel); - caveBranches.Add(tunnel); - - int branches = Rand.Range(caveParams.MinBranchCount, caveParams.MaxBranchCount, Rand.RandSync.Server); - for (int j = 0; j < branches; j++) - { - Tunnel parentBranch = caveBranches.GetRandom(Rand.RandSync.Server); - Vector2 branchStartPos = parentBranch.Nodes[Rand.Int(parentBranch.Nodes.Count / 2, Rand.RandSync.Server)].ToVector2(); - Vector2 branchEndPos = parentBranch.Nodes[Rand.Range(parentBranch.Nodes.Count / 2, parentBranch.Nodes.Count, Rand.RandSync.Server)].ToVector2(); - var branchSegments = MathUtils.GenerateJaggedLine( - branchStartPos, branchEndPos, - iterations: 3, - offsetAmount: Vector2.Distance(branchStartPos, branchEndPos) * 0.75f); - if (!branchSegments.Any()) { continue; } - - var branch = new Tunnel(TunnelType.Cave, SegmentsToNodes(branchSegments), 0, parentBranch); - Tunnels.Add(branch); - caveBranches.Add(branch); - } - - foreach (Tunnel branch in caveBranches) - { - PositionsOfInterest.Add(new InterestingPosition(branch.Nodes.Last(), PositionType.Cave)); - cave.Tunnels.Add(branch); - } - - static List SegmentsToNodes(List segments) - { - List nodes = new List(); - foreach (Vector2[] segment in segments) - { - nodes.Add(segment[0].ToPoint()); - } - nodes.Add(segments.Last()[1].ToPoint()); - return nodes; - } + GenerateCave(caveParams, parentTunnel, cavePos, caveSize); CalculateTunnelDistanceField(density: 1000); } } - private void GenerateRuin(List mainPath, bool mirror) + private void GenerateCave(CaveGenerationParams caveParams, Tunnel parentTunnel, Point cavePos, Point caveSize) + { + Rectangle caveArea = new Rectangle(cavePos - new Point(caveSize.X / 2, caveSize.Y / 2), caveSize); + Point closestParentNode = parentTunnel.Nodes.First(); + double closestDist = double.PositiveInfinity; + foreach (Point node in parentTunnel.Nodes) + { + if (caveArea.Contains(node)) { continue; } + double dist = MathUtils.DistanceSquared((double)node.X, (double)node.Y, (double)cavePos.X, (double)cavePos.Y); + if (dist < closestDist) + { + closestParentNode = node; + closestDist = dist; + } + } + + if (!MathUtils.GetLineRectangleIntersection(closestParentNode.ToVector2(), cavePos.ToVector2(), new Rectangle(caveArea.X, caveArea.Y + caveArea.Height, caveArea.Width, caveArea.Height), out Vector2 caveStartPosVector)) + { + caveStartPosVector = caveArea.Location.ToVector2(); + } + + Point caveStartPos = caveStartPosVector.ToPoint(); + Point caveEndPos = cavePos - (caveStartPos - cavePos); + + Cave cave = new Cave(caveParams, caveArea, caveStartPos, caveEndPos); + Caves.Add(cave); + + var caveSegments = MathUtils.GenerateJaggedLine( + caveStartPos.ToVector2(), caveEndPos.ToVector2(), + iterations: 3, + offsetAmount: Vector2.Distance(caveStartPos.ToVector2(), caveEndPos.ToVector2()) * 0.75f, + bounds: caveArea); + + if (!caveSegments.Any()) { return; } + + List caveBranches = new List(); + + var tunnel = new Tunnel(TunnelType.Cave, SegmentsToNodes(caveSegments), 100, parentTunnel); + Tunnels.Add(tunnel); + caveBranches.Add(tunnel); + + int branches = Rand.Range(caveParams.MinBranchCount, caveParams.MaxBranchCount, Rand.RandSync.Server); + for (int j = 0; j < branches; j++) + { + Tunnel parentBranch = caveBranches.GetRandom(Rand.RandSync.Server); + Vector2 branchStartPos = parentBranch.Nodes[Rand.Int(parentBranch.Nodes.Count / 2, Rand.RandSync.Server)].ToVector2(); + Vector2 branchEndPos = parentBranch.Nodes[Rand.Range(parentBranch.Nodes.Count / 2, parentBranch.Nodes.Count, Rand.RandSync.Server)].ToVector2(); + var branchSegments = MathUtils.GenerateJaggedLine( + branchStartPos, branchEndPos, + iterations: 3, + offsetAmount: Vector2.Distance(branchStartPos, branchEndPos) * 0.75f, + bounds: caveArea); + if (!branchSegments.Any()) { continue; } + + var branch = new Tunnel(TunnelType.Cave, SegmentsToNodes(branchSegments), 0, parentBranch); + Tunnels.Add(branch); + caveBranches.Add(branch); + } + + foreach (Tunnel branch in caveBranches) + { + var node = branch.Nodes.Last(); + PositionsOfInterest.Add(new InterestingPosition(node, node.Y < AbyssArea.Bottom ? PositionType.AbyssCave : PositionType.Cave, cave)); + cave.Tunnels.Add(branch); + } + + static List SegmentsToNodes(List segments) + { + List nodes = new List(); + foreach (Vector2[] segment in segments) + { + nodes.Add(segment[0].ToPoint()); + } + nodes.Add(segments.Last()[1].ToPoint()); + return nodes; + } + } + + private void GenerateRuin(Tunnel mainPath, bool mirror) { var ruinGenerationParams = RuinGenerationParams.GetRandom(); @@ -1424,12 +1689,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) @@ -1495,32 +1760,32 @@ 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()) + var validPoints = distanceField.FindAll(d => d.distance >= minDistance && (limits == null || limits.Value.Contains(d.point))); + validPoints.RemoveAll(d => d.point.Y < GetBottomPosition(d.point.X).Y + minDistance); + if (asCloseAsPossible || !validPoints.Any()) { if (!validPoints.Any()) { validPoints = distanceField; } - Pair furthestPoint = null; + (Point position, double distance) closestPoint = validPoints.First(); foreach (var point in validPoints) { - if (furthestPoint == null || point.Second > furthestPoint.Second) + if (point.distance < closestPoint.distance) { - furthestPoint = point; + closestPoint = point; } } - return furthestPoint.First; + return closestPoint.position; } else { - return validPoints[Rand.Int(validPoints.Count, Rand.RandSync.Server)].First; + return validPoints[Rand.Int(validPoints.Count, Rand.RandSync.Server)].point; } } private void CalculateTunnelDistanceField(int density) { - distanceField = new List>(); + distanceField = new List<(Point point, double distance)>(); for (int x = 0; x < Size.X; x += density) { for (int y = 0; y < Size.Y; y += density) @@ -1536,7 +1801,7 @@ namespace Barotrauma } shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.DistanceSquared((double)point.X, (double)point.Y, (double)startPosition.X, (double)startPosition.Y)); shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.DistanceSquared((double)point.X, (double)point.Y, (double)endPosition.X, (double)endPosition.Y)); - distanceField.Add(new Pair(point, Math.Sqrt(shortestDistSqr))); + distanceField.Add((point, Math.Sqrt(shortestDistSqr))); } } } @@ -1570,6 +1835,7 @@ namespace Barotrauma vertices.Add(edge.Point2); } } + if (vertices.Count < 3) { return null; } return CreateIceChunk(vertices.Select(v => v - position).ToList(), position, health); } @@ -1588,6 +1854,8 @@ namespace Barotrauma private DestructibleLevelWall CreateIceSpire(List usedSpireEdges) { + const float maxLength = 15000.0f; + var mainPathPos = PositionsOfInterest.Where(pos => pos.PositionType == PositionType.MainPath).GetRandom(Rand.RandSync.Server); double closestDistSqr = double.PositiveInfinity; GraphEdge closestEdge = null; @@ -1595,12 +1863,16 @@ namespace Barotrauma foreach (VoronoiCell cell in cells) { if (cell.CellType != CellType.Solid) { continue; } - //don't spawn spires near the start/end of the level - if (cell.Center.X < Size.X * 0.2f || cell.Center.X > Size.X * 0.8f) { continue; } foreach (GraphEdge edge in cell.Edges) { if (!edge.IsSolid || usedSpireEdges.Contains(edge) || edge.NextToCave) { continue; } + //don't spawn spires near the start/end of the level + if (edge.Center.Y > Size.Y / 2 && (edge.Center.X < Size.X * 0.3f || edge.Center.X > Size.X * 0.7f)) { continue; } + if (Vector2.DistanceSquared(edge.Center, StartPosition) < maxLength * maxLength) { continue; } + if (Vector2.DistanceSquared(edge.Center, EndPosition) < maxLength * maxLength) { continue; } + //don't spawn on very long edges if (Vector2.DistanceSquared(edge.Point1, edge.Point2) > 1000.0f * 1000.0f) { continue; } + //don't spawn on edges facing away from the main path if (Vector2.Dot(Vector2.Normalize(mainPathPos.Position.ToVector2()) - edge.Center, edge.GetNormal(cell)) < 0.5f) { continue; } double distSqr = MathUtils.DistanceSquared(edge.Center.X, edge.Center.Y, mainPathPos.Position.X, mainPathPos.Position.Y); if (distSqr < closestDistSqr) @@ -1617,7 +1889,9 @@ namespace Barotrauma usedSpireEdges.Add(closestEdge); Vector2 edgeNormal = closestEdge.GetNormal(closestCell); - float spireLength = (float)Math.Min(Math.Sqrt(closestDistSqr), 15000.0f); + 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() @@ -1729,7 +2003,8 @@ namespace Barotrauma { string levelName = GenerationParams.Identifier.ToLowerInvariant(); float minCommonness = float.MaxValue, maxCommonness = float.MinValue; - List> levelResources = new List>(); + List<(ItemPrefab itemPrefab, float commonness)> levelResources = new List<(ItemPrefab itemPrefab, float commonness)>(); + var fixedResources = new List<(ItemPrefab itemPrefab, ItemPrefab.FixedQuantityResourceInfo resourceInfo)>(); foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs) { if (itemPrefab.LevelCommonness.TryGetValue(levelName, out float commonness) || @@ -1738,15 +2013,67 @@ namespace Barotrauma if (commonness <= 0.0f) { continue; } if (commonness < minCommonness) { minCommonness = commonness; } if (commonness > maxCommonness) { maxCommonness = commonness; } - levelResources.Add(new Pair(itemPrefab, commonness)); + levelResources.Add((itemPrefab, commonness)); + } + else if (itemPrefab.LevelQuantity.TryGetValue(levelName, out var fixedQuantityResourceInfo) || + itemPrefab.LevelQuantity.TryGetValue("", out fixedQuantityResourceInfo)) + { + fixedResources.Add((itemPrefab, fixedQuantityResourceInfo)); + } + } + levelResources.Sort((x, y) => x.commonness.CompareTo(y.commonness)); + + DebugConsole.Log("Generating level resources..."); + var allValidLocations = GetAllValidClusterLocations(); + var maxResourceOverlap = 0.4f; + + foreach (var (itemPrefab, resourceInfo) in fixedResources) + { + for (int i = 0; i < resourceInfo.ClusterQuantity; i++) + { + var location = allValidLocations.GetRandom(l => + { + if (l.Cell == null || l.Edge == null) { return false; } + if (resourceInfo.IsIslandSpecifc && !l.Cell.Island) { return false; } + if (l.EdgeCenter.Y < AbyssArea.Bottom) { return false; } + return resourceInfo.ClusterSize <= GetMaxResourcesOnEdge(itemPrefab, l, out _); + + }, randSync: Rand.RandSync.Server); + + if (location.Cell == null || location.Edge == null) { break; } + + PlaceResources(itemPrefab, resourceInfo.ClusterSize, location, out _); + var locationIndex = allValidLocations.FindIndex(l => l.Equals(location)); + allValidLocations.RemoveAt(locationIndex); } } - DebugConsole.Log("Generating level resources..."); + //place some of the least common resources in the abyss + for (int j = 0; j < levelResources.Count && j < 5; j++) + { + for (int i = 0; i < 10; i++) + { + var (itemPrefab, commonness) = levelResources[j]; + var location = allValidLocations.GetRandom(l => + { + if (l.Cell == null || l.Edge == null) { return false; } + if (l.EdgeCenter.Y > AbyssArea.Bottom) { return false; } + l.InitializeResources(); + return l.Resources.Count <= GetMaxResourcesOnEdge(itemPrefab, l, out _); + + }, randSync: Rand.RandSync.Server); + + if (location.Cell == null || location.Edge == null) { break; } + int clusterSize = Rand.Range(GenerationParams.ResourceClusterSizeRange.X, GenerationParams.ResourceClusterSizeRange.Y, Rand.RandSync.Server); + PlaceResources(itemPrefab, clusterSize, location, out _); + var locationIndex = allValidLocations.FindIndex(l => l.Equals(location)); + allValidLocations.RemoveAt(locationIndex); + } + } PathPoints.Clear(); nextPathPointId = 0; - + foreach (Tunnel tunnel in Tunnels) { var tunnelLength = 0.0f; @@ -1797,9 +2124,7 @@ namespace Barotrauma } int itemCount = 0; - var allValidLocations = GetAllValidClusterLocations(); string[] exclusiveResourceTags = new string[2] { "ore", "plant" }; - var maxResourceOverlap = 0.4f; // Create first cluster for each spawn point foreach (var pathPoint in PathPoints.Where(p => p.ShouldContainResources)) @@ -1870,6 +2195,7 @@ namespace Barotrauma { var validLocation = allValidLocations[i]; if (!IsNextToTunnelType(validLocation.Edge, pathPoint.TunnelType)) { continue; } + if (validLocation.EdgeCenter.Y < AbyssArea.Bottom) { continue; } var distanceSquaredToEdge = Vector2.DistanceSquared(pathPoint.Position, validLocation.EdgeCenter); // Edge isn't too far from the path point if (distanceSquaredToEdge > 3.0f * (intervalRange.Y * intervalRange.Y)) { continue; } @@ -2016,8 +2342,8 @@ namespace Barotrauma if (pathPoint.ClusterLocations.Count == 0) { selectedPrefab = ToolBox.SelectWeightedRandom( - levelResources.Select(it => it.First).ToList(), - levelResources.Select(it => it.Second).ToList(), + levelResources.Select(it => it.itemPrefab).ToList(), + levelResources.Select(it => it.commonness).ToList(), Rand.RandSync.Server); selectedPrefab.Tags.ForEach(t => { @@ -2030,22 +2356,21 @@ namespace Barotrauma else { var filteredResources = levelResources.Where(it => - !pathPoint.ResourceIds.Contains(it.First.Identifier) && - pathPoint.ResourceTags.Any() && it.First.Tags.Any(t => pathPoint.ResourceTags.Contains(t))); + !pathPoint.ResourceIds.Contains(it.itemPrefab.Identifier) && + pathPoint.ResourceTags.Any() && it.itemPrefab.Tags.Any(t => pathPoint.ResourceTags.Contains(t))); selectedPrefab = ToolBox.SelectWeightedRandom( - filteredResources.Select(it => it.First).ToList(), - filteredResources.Select(it => it.Second).ToList(), + filteredResources.Select(it => it.itemPrefab).ToList(), + filteredResources.Select(it => it.commonness).ToList(), Rand.RandSync.Server); } if (selectedPrefab == null) { return false; } // Create resources for the cluster - var commonness = levelResources.First(r => r.First == selectedPrefab).Second; + var commonness = levelResources.First(r => r.itemPrefab == selectedPrefab).commonness; var lerpAmount = MathUtils.InverseLerp(minCommonness, maxCommonness, commonness); var maxClusterSize = (int)MathHelper.Lerp(GenerationParams.ResourceClusterSizeRange.X, GenerationParams.ResourceClusterSizeRange.Y, lerpAmount); - var edgeLength = Vector2.Distance(location.Edge.Point1, location.Edge.Point2); - var maxFitOnEdge = (int)Math.Floor(edgeLength / ((1.0f - maxResourceOverlap) * selectedPrefab.Size.X)); + var maxFitOnEdge = GetMaxResourcesOnEdge(selectedPrefab, location, out var edgeLength); maxClusterSize = Math.Min(maxClusterSize, maxFitOnEdge); if (itemCount + maxClusterSize > GenerationParams.ItemCount) { @@ -2059,7 +2384,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); @@ -2068,6 +2393,14 @@ namespace Barotrauma return true; } + + int GetMaxResourcesOnEdge(ItemPrefab resourcePrefab, ClusterLocation location, out float edgeLength) + { + edgeLength = 0.0f; + if (location.Cell == null || location.Edge == null) { return 0; } + edgeLength = Vector2.Distance(location.Edge.Point1, location.Edge.Point2); + return (int)Math.Floor(edgeLength / ((1.0f - maxResourceOverlap) * resourcePrefab.Size.X)); + } } /// Used by clients to set the rotation for the resources @@ -2086,7 +2419,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); } @@ -2119,6 +2452,10 @@ namespace Barotrauma private List GetAllValidClusterLocations() { + var subBorders = new List(); + Wrecks.ForEach(w => AddBordersToList(w)); + AddBordersToList(BeaconStation); + var locations = new List(); foreach (var c in GetAllCells()) { @@ -2133,21 +2470,54 @@ namespace Barotrauma } return locations; + void AddBordersToList(Submarine s) + { + if (s == null) { return; } + var rect = Submarine.AbsRect(s.WorldPosition, s.Borders.Size.ToVector2()); + subBorders.Add(rect); + } + bool IsValidEdge(GraphEdge e) { if (!e.IsSolid) { return false; } if (e.OutsideLevel) { return false; } - return ExtraWalls.None(w => w.Cells.Any(c => c.IsPointInside(e.Center) || - c.IsPointInside(e.Center - 100 * e.GetNormal(c)) || - c.Edges.Any(extraWallEdge => extraWallEdge == e))); + var eCenter = e.Center; + if (IsBlockedByWreckOrBeacon()) { return false; } + if (IsBlockedByWall()) { return false; } + return true; + + bool IsBlockedByWreckOrBeacon() + { + foreach (var r in subBorders) + { + if (Submarine.RectContains(r, e.Point1)) { return true; } + if (Submarine.RectContains(r, e.Point2)) { return true; } + if (Submarine.RectContains(r, eCenter)) { return true; } + } + return false; + } + + bool IsBlockedByWall() + { + foreach (var w in ExtraWalls) + { + foreach (var c in w.Cells) + { + if (c.IsPointInside(eCenter)) { return true; } + if (c.IsPointInside(eCenter - 100 * e.GetNormal(c))) { return true; } + if (c.Edges.Any(extraWallEdge => extraWallEdge == e)) { return true; } + } + } + return false; + } } } 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; @@ -2155,7 +2525,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); @@ -2165,7 +2535,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.Rect.Height / 2, 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(); @@ -2173,11 +2545,15 @@ namespace Barotrauma item.Rotation = MathHelper.ToDegrees(-MathUtils.VectorToAngle(edgeNormal) + MathHelper.PiOver2); #endif } + else if (item.body != null) + { + item.body.SetTransformIgnoreContacts(item.body.SimPosition, MathUtils.VectorToAngle(edgeNormal) - MathHelper.PiOver2); + } placedResources.Add(item); } } - 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()) { @@ -2189,7 +2565,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))) @@ -2202,7 +2578,7 @@ namespace Barotrauma if (Submarine.PickBody( ConvertUnits.ToSimUnits(startPos), ConvertUnits.ToSimUnits(endPos), - ExtraWalls.Where(w => w.Body != null && w.Body.BodyType == BodyType.Dynamic).Select(w => w.Body), + ExtraWalls.Where(w => w.Body?.BodyType == BodyType.Dynamic || w is DestructibleLevelWall).Select(w => w.Body), Physics.CollisionLevel | Physics.CollisionWall) != null) { position = ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition) + Vector2.Normalize(startPos - endPos) * offsetFromWall; @@ -2221,14 +2597,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()) { @@ -2237,8 +2613,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())))); } @@ -2330,12 +2710,13 @@ namespace Barotrauma public Vector2 GetBottomPosition(float xPosition) { int index = (int)Math.Floor(xPosition / Size.X * (bottomPositions.Count - 1)); - if (index < 0 || index >= bottomPositions.Count - 1) return new Vector2(xPosition, BottomPos); + if (index < 0 || index >= bottomPositions.Count - 1) { return new Vector2(xPosition, BottomPos); } - float yPos = MathHelper.Lerp( - bottomPositions[index].Y, - bottomPositions[index + 1].Y, - (xPosition - bottomPositions[index].X) / (bottomPositions[index + 1].X - bottomPositions[index].X)); + float t = (xPosition - bottomPositions[index].X) / (bottomPositions[index + 1].X - bottomPositions[index].X); + Debug.Assert(t < 1.0f); + t = MathHelper.Clamp(t, 0.0f, 1.0f); + + float yPos = MathHelper.Lerp(bottomPositions[index].Y, bottomPositions[index + 1].Y, t); return new Vector2(xPosition, yPos); } @@ -2382,10 +2763,42 @@ namespace Barotrauma tempCells.Add(cell); } } + + foreach (var abyssIsland in AbyssIslands) + { + if (abyssIsland.Area.X > worldPos.X + searchDepth * GridCellSize) { continue; } + if (abyssIsland.Area.Right < worldPos.X - searchDepth * GridCellSize) { continue; } + if (abyssIsland.Area.Y > worldPos.Y + searchDepth * GridCellSize) { continue; } + if (abyssIsland.Area.Bottom < worldPos.Y - searchDepth * GridCellSize) { continue; } + + tempCells.AddRange(abyssIsland.Cells); + } return tempCells; } + public VoronoiCell GetClosestCell(Vector2 worldPos) + { + double closestDist = double.MaxValue; + VoronoiCell closestCell = null; + int searchDepth = 2; + while (searchDepth < 5) + { + foreach (var cell in GetCells(worldPos, searchDepth)) + { + double dist = MathUtils.DistanceSquared(cell.Site.Coord.X, cell.Site.Coord.Y, worldPos.X, worldPos.Y); + if (dist < closestDist) + { + closestDist = dist; + closestCell = cell; + } + } + if (closestCell != null) { break; } + searchDepth++; + } + return closestCell; + } + private void CreatePathToClosestTunnel(Point pos) { VoronoiCell closestPathCell = null; @@ -2422,26 +2835,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; } @@ -2531,7 +2956,7 @@ namespace Barotrauma } } // Only spawn thalamus when the wreck has some thalamus items defined. - if (Rand.Value(Rand.RandSync.Server) <= Loaded.GenerationParams.ThalamusProbability && sub.GetItems(false).Any(i => i.Prefab.Category == MapEntityCategory.Thalamus)) + if (Rand.Value(Rand.RandSync.Server) <= Loaded.GenerationParams.ThalamusProbability && sub.GetItems(false).Any(i => i.Prefab.HasSubCategory("thalamus"))) { if (!sub.CreateWreckAI()) { @@ -2548,7 +2973,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)"); @@ -2742,6 +3167,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))); } } @@ -2794,25 +3225,28 @@ namespace Barotrauma bool isStart = (i == 0) == !Mirrored; if (isStart) { - //only create a starting outpost in campaign and tutorial modes + if (LevelData.Type != LevelData.LevelType.Outpost) + { + //only create a starting outpost in campaign and tutorial modes #if CLIENT - if (Screen.Selected != GameMain.LevelEditorScreen && !IsModeStartOutpostCompatible()) - { - continue; - } + if (Screen.Selected != GameMain.LevelEditorScreen && !IsModeStartOutpostCompatible()) + { + continue; + } #else - if (!IsModeStartOutpostCompatible()) - { - continue; - } + if (!IsModeStartOutpostCompatible()) + { + continue; + } #endif - if (StartLocation != null && !StartLocation.Type.HasOutpost) { continue; } + } + if (StartLocation != null && !StartLocation.HasOutpost()) { continue; } } else { //don't create an end outpost for locations if (LevelData.Type == LevelData.LevelType.Outpost) { continue; } - if (EndLocation != null && !EndLocation.Type.HasOutpost) { continue; } + if (EndLocation != null && !EndLocation.HasOutpost()) { continue; } } SubmarineInfo outpostInfo; @@ -2868,6 +3302,14 @@ namespace Barotrauma DebugConsole.NewMessage($"Generating an outpost for the {(isStart ? "start" : "end")} of the level... (Location type: {locationType}, level type: {LevelData.Type})"); outpost = OutpostGenerator.Generate(outpostGenerationParams, locationType, onlyEntrance: LevelData.Type != LevelData.LevelType.Outpost); } + + foreach (string categoryToHide in locationType.HideEntitySubcategories) + { + foreach (MapEntity entityToHide in MapEntity.mapEntityList.Where(me => me.Submarine == outpost && me.prefab.HasSubCategory(categoryToHide))) + { + entityToHide.HiddenInGame = true; + } + } } else { @@ -2981,8 +3423,14 @@ namespace Barotrauma string beaconStationName = System.IO.Path.GetFileNameWithoutExtension(contentFile.Path); BeaconStation = SpawnSubOnPath(beaconStationName, contentFile, SubmarineType.BeaconStation); + if (BeaconStation == null) { return; } Item sonarItem = Item.ItemList.Find(it => it.Submarine == BeaconStation && it.GetComponent() != null); + if (sonarItem == null) + { + DebugConsole.ThrowError($"No sonar found in the beacon station \"{beaconStationName}\"!"); + return; + } beaconSonar = sonarItem.GetComponent(); } @@ -3016,15 +3464,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); @@ -3034,6 +3493,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); @@ -3124,7 +3584,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 f7be386df..41345163c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs @@ -29,12 +29,19 @@ namespace Barotrauma public bool HasBeaconStation; public bool IsBeaconActive; + public bool HasHuntingGrounds; + public OutpostGenerationParams ForceOutpostGenerationParams; public readonly Point Size; 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(); @@ -81,6 +88,8 @@ namespace Barotrauma HasBeaconStation = element.GetAttributeBool("hasbeaconstation", false); IsBeaconActive = element.GetAttributeBool("isbeaconactive", false); + HasHuntingGrounds = element.GetAttributeBool("hashuntinggrounds", false); + string generationParamsId = element.GetAttributeString("generationparams", ""); GenerationParams = LevelGenerationParams.LevelParams.Find(l => l.Identifier == generationParamsId || l.OldIdentifier == generationParamsId); if (GenerationParams == null) @@ -96,7 +105,7 @@ namespace Barotrauma InitialDepth = element.GetAttributeInt("initialdepth", GenerationParams.InitialDepthMin); string biomeIdentifier = element.GetAttributeString("biome", ""); - Biome = LevelGenerationParams.GetBiomes().FirstOrDefault(b => b.Identifier == biomeIdentifier); + Biome = LevelGenerationParams.GetBiomes().FirstOrDefault(b => b.Identifier == biomeIdentifier || b.OldIdentifier == biomeIdentifier); if (Biome == null) { DebugConsole.ThrowError($"Error in level data: could not find the biome \"{biomeIdentifier}\"."); @@ -135,7 +144,10 @@ namespace Barotrauma var rand = new MTRandom(ToolBox.StringToInt(Seed)); InitialDepth = (int)MathHelper.Lerp(GenerationParams.InitialDepthMin, GenerationParams.InitialDepthMax, (float)rand.NextDouble()); - HasBeaconStation = rand.NextDouble() < locationConnection.Locations.Select(l => l.Type.BeaconStationChance).Max(); + float maxHuntingGroundsProbability = 0.3f; + HasHuntingGrounds = rand.NextDouble() < Difficulty / 100.0f * maxHuntingGroundsProbability; + + HasBeaconStation = !HasHuntingGrounds && rand.NextDouble() < locationConnection.Locations.Select(l => l.Type.BeaconStationChance).Max(); IsBeaconActive = false; } @@ -158,7 +170,7 @@ namespace Barotrauma (int)MathUtils.Round(GenerationParams.Height, Level.GridCellSize)); } - public static LevelData CreateRandom(string seed = "", float? difficulty = null, LevelGenerationParams generationParams = null) + public static LevelData CreateRandom(string seed = "", float? difficulty = null, LevelGenerationParams generationParams = null, bool requireOutpost = false) { if (string.IsNullOrEmpty(seed)) { @@ -167,25 +179,34 @@ namespace Barotrauma Rand.SetSyncedSeed(ToolBox.StringToInt(seed)); - LevelType type = generationParams == null ? LevelData.LevelType.LocationConnection : generationParams.Type; + LevelType type = generationParams == null ? + (requireOutpost ? LevelType.Outpost : LevelType.LocationConnection) : + generationParams.Type; if (generationParams == null) { generationParams = LevelGenerationParams.GetRandom(seed, type); } var biome = LevelGenerationParams.GetBiomes().FirstOrDefault(b => generationParams.AllowedBiomes.Contains(b)) ?? LevelGenerationParams.GetBiomes().GetRandom(Rand.RandSync.Server); - float beaconRng = Rand.Range(0.0f, 1.0f, Rand.RandSync.Server); var levelData = new LevelData( seed, difficulty ?? Rand.Range(30.0f, 80.0f, Rand.RandSync.Server), Rand.Range(0.0f, 1.0f, Rand.RandSync.Server), generationParams, - biome) + biome); + if (type == LevelType.LocationConnection) { - HasBeaconStation = beaconRng < 0.5f, - IsBeaconActive = beaconRng > 0.25f - }; - GameMain.GameSession?.GameMode?.Mission?.AdjustLevelData(levelData); + float beaconRng = Rand.Range(0.0f, 1.0f, Rand.RandSync.Server); + levelData.HasBeaconStation = beaconRng < 0.5f; + levelData.IsBeaconActive = beaconRng > 0.25f; + } + if (GameMain.GameSession?.GameMode != null) + { + foreach (Mission mission in GameMain.GameSession.GameMode.Missions) + { + mission.AdjustLevelData(levelData); + } + } return levelData; } @@ -207,6 +228,13 @@ namespace Barotrauma new XAttribute("isbeaconactive", IsBeaconActive.ToString())); } + if (HasHuntingGrounds) + { + newElement.Add( + new XAttribute("hashuntinggrounds", HasHuntingGrounds.ToString())); + + } + if (Type == LevelType.Outpost) { if (EventHistory.Any()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs index 74b557453..943dc6768 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs @@ -195,25 +195,25 @@ namespace Barotrauma set; } - [Serialize(100000, true), Editable(MinValueInt = 10000, MaxValueInt = 1000000)] + [Serialize(100000, true), Editable] public int MinWidth { get { return minWidth; } - set { minWidth = Math.Max(value, 2000); } + set { minWidth = MathHelper.Clamp(value, 2000, 1000000); } } - [Serialize(100000, true), Editable(MinValueInt = 10000, MaxValueInt = 1000000)] + [Serialize(100000, true), Editable] public int MaxWidth { get { return maxWidth; } - set { maxWidth = Math.Max(value, 2000); } + set { maxWidth = MathHelper.Clamp(value, 2000, 1000000); } } - [Serialize(50000, true), Editable(MinValueInt = 10000, MaxValueInt = 1000000)] + [Serialize(50000, true), Editable] public int Height { get { return height; } - set { height = Math.Max(value, 2000); } + set { height = MathHelper.Clamp(value, 2000, 1000000); } } [Serialize(80000, true), Editable(MinValueInt = 0, MaxValueInt = 1000000)] @@ -404,7 +404,35 @@ namespace Barotrauma set; } - [Serialize(300000, true, description: "How far below the level the sea floor is placed."), Editable(MinValueFloat = Level.MaxEntityDepth, MaxValueFloat = 0.0f)] + [Serialize(5, true), Editable(MinValueInt = 0, MaxValueInt = 20)] + public int AbyssIslandCount + { + get; + set; + } + + [Serialize("4000,7000", true), Editable] + public Point AbyssIslandSizeMin + { + get; + set; + } + + [Serialize("8000,10000", true), Editable] + public Point AbyssIslandSizeMax + { + get; + set; + } + + [Serialize(0.5f, true), Editable()] + public float AbyssIslandCaveProbability + { + get; + set; + } + + [Serialize(-300000, true, description: "How far below the level the sea floor is placed."), Editable(MinValueFloat = Level.MaxEntityDepth, MaxValueFloat = 0.0f)] public int SeaFloorDepth { get { return seaFloorBaseDepth; } @@ -554,7 +582,7 @@ namespace Barotrauma var matchingLevelParams = LevelParams.FindAll(lp => lp.Type == type && lp.allowedBiomes.Any()); if (biome == null) { - matchingLevelParams = matchingLevelParams.FindAll(lp => !lp.allowedBiomes.Any(b => b.IsEndBiome)); + matchingLevelParams = matchingLevelParams.FindAll(lp => !lp.allowedBiomes.All(b => b.IsEndBiome)); } else { 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 f5f6a9667..238743d34 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -151,10 +151,10 @@ namespace Barotrauma } } - availableSpawnPositions.Clear(); foreach (Level.Cave cave in level.Caves) { availablePrefabs = new List(LevelObjectPrefab.List.FindAll(p => p.SpawnPos.HasFlag(LevelObjectPrefab.SpawnPosType.CaveWall))); + availableSpawnPositions.Clear(); suitableSpawnPositions.Clear(); spawnPositionWeights.Clear(); @@ -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,19 +184,63 @@ 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); } } - } + } } } - 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; } } } @@ -372,7 +418,7 @@ namespace Barotrauma return objects; } - private readonly static List objectsInRange = new List(); + private readonly static HashSet objectsInRange = new HashSet(); public IEnumerable GetAllObjects(Vector2 worldPosition, float radius) { var minIndices = GetGridIndices(worldPosition - Vector2.One * radius); @@ -391,10 +437,10 @@ namespace Barotrauma { for (int y = minIndices.Y; y <= maxIndices.Y; y++) { - if (objectGrid[x, y] == null) continue; + if (objectGrid[x, y] == null) { continue; } foreach (LevelObject obj in objectGrid[x, y]) { - if (!objectsInRange.Contains(obj)) objectsInRange.Add(obj); + objectsInRange.Add(obj); } } } @@ -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 68fba44da..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, } @@ -319,7 +320,7 @@ namespace Barotrauma } else if (List.Any()) { - DebugConsole.NewMessage($"Loading additional level object prefabs from file '{configPath}'"); + DebugConsole.Log($"Loading additional level object prefabs from file '{configPath}'"); } foreach (XElement subElement in mainElement.Elements()) { @@ -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 dfa348e8a..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); } } @@ -528,7 +553,7 @@ namespace Barotrauma float structureDamage = attack.GetStructureDamage(deltaTime); if (structureDamage > 0.0f) { - Explosion.RangedStructureDamage(worldPosition, attack.DamageRange, structureDamage, damageLevelWalls: false); + Explosion.RangedStructureDamage(worldPosition, attack.DamageRange, structureDamage, levelWallDamage: 0.0f); } } @@ -618,7 +643,7 @@ namespace Barotrauma public Vector2 GetWaterFlowVelocity() { - if (Force == Vector2.Zero) return Vector2.Zero; + if (Force == Vector2.Zero || ForceMode == TriggerForceMode.LimitVelocity) { return Vector2.Zero; } Vector2 vel = Force; if (ForceMode == TriggerForceMode.Acceleration) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelWall.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelWall.cs index e18495c94..f4d08b8a0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelWall.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelWall.cs @@ -36,7 +36,16 @@ namespace Barotrauma } } - public float WallDamageOnTouch; + private float wallDamageOnTouch; + public float WallDamageOnTouch + { + get { return wallDamageOnTouch; } + set + { + Cells.ForEach(c => c.DoesDamage = !MathUtils.NearlyEqual(value, 0.0f)); + wallDamageOnTouch = value; + } + } public float MoveSpeed; 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..ac4c95bc1 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); @@ -289,7 +297,8 @@ namespace Barotrauma { originalMyPortID = myPort.Item.ID; - myPort.Undock(); + myPort.Undock(applyEffects: false); + 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, @@ -313,7 +322,7 @@ namespace Barotrauma sub.SetPosition((linkedPort.Item.WorldPosition - portDiff) - offset); myPort.Dock(linkedPort); - myPort.Lock(true); + myPort.Lock(true, applyEffects: false); } } @@ -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,20 @@ 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("previewimage") != null) + { + saveElement.Attribute("previewimage").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 1e0bd3eea..0ded8788e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -33,8 +33,9 @@ namespace Barotrauma { OriginalContainerID = item.OriginalContainerID; } + OriginalID = item.ID; - ModuleIndex = (ushort)item.OriginalModuleIndex; + ModuleIndex = (ushort) item.OriginalModuleIndex; Identifier = item.prefab.Identifier; } @@ -42,6 +43,7 @@ namespace Barotrauma { return obj.OriginalID == OriginalID && obj.OriginalContainerID == OriginalContainerID && obj.ModuleIndex == ModuleIndex && obj.Identifier == Identifier; } + public bool Matches(Item item) { if (item.OriginalContainerID != Entity.NullEntityID) @@ -56,13 +58,17 @@ namespace Barotrauma } public readonly List Connections = new List(); - + private string baseName; private int nameFormatIndex; public bool Discovered; - public int TypeChangeTimer; + public readonly Dictionary ProximityTimer = new Dictionary(); + public (LocationTypeChange typeChange, int delay, MissionPrefab parentMission)? PendingLocationTypeChange; + public int LocationTypeChangeCooldown; + + public readonly int ZoneIndex; public string BaseName { get => baseName; } @@ -80,14 +86,76 @@ namespace Barotrauma public Reputation Reputation { get; set; } - public int[] ProximityTime { get; private set; } + public int TurnsInRadiation { 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 @@ -106,12 +174,11 @@ namespace Barotrauma { get { - availableMissions.RemoveAll(m => m.Completed || m.Failed); + availableMissions.RemoveAll(m => m.Completed || (m.Failed && !m.Prefab.AllowRetry)); return availableMissions; } } - public Mission SelectedMission { get; @@ -152,6 +219,10 @@ namespace Barotrauma public string LastTypeChangeMessage; + public int TimeSinceLastTypeChange; + + public bool IsGateBetweenBiomes; + private struct LoadedMission { public MissionPrefab MissionPrefab { get; } @@ -175,29 +246,49 @@ namespace Barotrauma return $"Location ({Name ?? "null"})"; } - public Location(Vector2 mapPosition, int? zone, Random rand, bool requireOutpost = false, IEnumerable existingLocations = null) + public Location(Vector2 mapPosition, int? zone, Random rand, bool requireOutpost = false, LocationType? forceLocationType = null, IEnumerable existingLocations = null) { - Type = LocationType.Random(rand, zone, requireOutpost); + Type = forceLocationType ?? LocationType.Random(rand, zone, requireOutpost); Name = RandomName(Type, rand, existingLocations); MapPosition = mapPosition; PortraitId = ToolBox.StringToInt(Name); - Connections = new List(); - ProximityTime = new int[Type.CanChangeTo.Count]; + Connections = new List(); } public Location(XElement element) { string locationType = element.GetAttributeString("type", ""); Type = LocationType.List.Find(lt => lt.Identifier.Equals(locationType, StringComparison.OrdinalIgnoreCase)); + bool typeNotFound = false; + if (Type == null) + { + DebugConsole.AddWarning($"Could not find location type \"{locationType}\". Using location type \"None\" instead."); + Type = LocationType.List.Find(lt => lt.Identifier.Equals("None", StringComparison.OrdinalIgnoreCase)); + Type ??= LocationType.List.First(); + typeNotFound = true; + } + 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); - ProximityTime = element.GetAttributeIntArray("proximitytime", new int[Type.CanChangeTo.Count]); - if (ProximityTime.Length != Type.CanChangeTo.Count) { ProximityTime = new int[Type.CanChangeTo.Count]; } - MechanicalPriceMultiplier = element.GetAttributeFloat("mechanicalpricemultipler", 1.0f); + IsGateBetweenBiomes = element.GetAttributeBool("isgatebetweenbiomes", false); + MechanicalPriceMultiplier = element.GetAttributeFloat("mechanicalpricemultipler", 1.0f); + TurnsInRadiation = element.GetAttributeInt(nameof(TurnsInRadiation).ToLower(), 0); + + if (!typeNotFound) + { + for (int i = 0; i < Type.CanChangeTo.Count; i++) + { + for (int j = 0; j < Type.CanChangeTo[i].Requirements.Count; j++) + { + ProximityTimer.Add(Type.CanChangeTo[i].Requirements[j], element.GetAttributeInt("proximitytimer" + i + "-" + j, 0)); + } + } + + LoadLocationTypeChange(element); + } string[] takenItemStr = element.GetAttributeStringArray("takenitems", new string[0]); foreach (string takenItem in takenItemStr) @@ -237,16 +328,42 @@ 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); } + public void LoadLocationTypeChange(XElement locationElement) + { + TimeSinceLastTypeChange = locationElement.GetAttributeInt("timesincelasttypechange", 0); + LocationTypeChangeCooldown = locationElement.GetAttributeInt("locationtypechangecooldown", 0); + foreach (XElement subElement in locationElement.Elements()) + { + switch (subElement.Name.ToString()) + { + case "pendinglocationtypechange": + int timer = subElement.GetAttributeInt("timer", 0); + if (subElement.Attribute("index") != null) + { + int locationTypeChangeIndex = subElement.GetAttributeInt("index", 0); + PendingLocationTypeChange = (Type.CanChangeTo[locationTypeChangeIndex], timer, null); + } + else + { + string missionIdentifier = subElement.GetAttributeString("missionidentifier", ""); + var mission = MissionPrefab.List.Find(mp => mp.Identifier.Equals(missionIdentifier, StringComparison.OrdinalIgnoreCase)); + if (mission == null) + { + DebugConsole.AddWarning($"Failed to activate a location type change from the mission \"{missionIdentifier}\" in location \"{Name}\". Matching mission not found."); + continue; + } + PendingLocationTypeChange = (mission.LocationTypeChangeOnCompleted, timer, mission); + } + break; + } + } + } + public void LoadMissions(XElement locationElement) { if (locationElement.GetChildElement("missions") is XElement missionsElement) @@ -266,9 +383,9 @@ namespace Barotrauma } - public static Location CreateRandom(Vector2 position, int? zone, Random rand, bool requireOutpost, IEnumerable existingLocations = null) + public static Location CreateRandom(Vector2 position, int? zone, Random rand, bool requireOutpost, LocationType? forceLocationType = null, IEnumerable existingLocations = null) { - return new Location(position, zone, rand, requireOutpost, existingLocations); + return new Location(position, zone, rand, requireOutpost, forceLocationType, existingLocations); } public void ChangeType(LocationType newType) @@ -278,15 +395,34 @@ namespace Barotrauma DebugConsole.Log("Location " + baseName + " changed it's type from " + Type + " to " + newType); Type = newType; - ProximityTime = new int[Type.CanChangeTo.Count]; - Name = Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", baseName); + Name = Type.NameFormats == null ? baseName : Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", baseName); + + if (Type.MissionIdentifiers.Any()) + { + UnlockMissionByIdentifier(Type.MissionIdentifiers.GetRandom()); + } + if (Type.MissionTags.Any()) + { + UnlockMissionByTag(Type.MissionTags.GetRandom()); + } + CreateStore(force: true); } public void UnlockMission(MissionPrefab missionPrefab, LocationConnection connection) { if (AvailableMissions.Any(m => m.Prefab == missionPrefab)) { return; } - var mission = InstantiateMission(missionPrefab, ref connection); + var mission = InstantiateMission(missionPrefab, connection); + availableMissions.Add(mission); +#if CLIENT + GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo(); +#endif + } + + public void UnlockMission(MissionPrefab missionPrefab) + { + if (AvailableMissions.Any(m => m.Prefab == missionPrefab)) { return; } + var mission = InstantiateMission(missionPrefab); availableMissions.Add(mission); #if CLIENT GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo(); @@ -304,8 +440,7 @@ namespace Barotrauma } else { - LocationConnection connection = null; - var mission = InstantiateMission(missionPrefab, ref connection); + var mission = InstantiateMission(missionPrefab, out LocationConnection connection); //don't allow duplicate missions in the same connection if (AvailableMissions.Any(m => m.Prefab == missionPrefab && m.Locations.Contains(mission.Locations[0]) && m.Locations.Contains(mission.Locations[1]))) { @@ -332,14 +467,13 @@ namespace Barotrauma var unusedMissions = matchingMissions.Where(m => !availableMissions.Any(mission => mission.Prefab == m)); if (unusedMissions.Any()) { - var suitableMissions = unusedMissions.Where(m => Connections.Any(c => m.IsAllowed(this, c.OtherLocation(this)))); + var suitableMissions = unusedMissions.Where(m => Connections.Any(c => m.IsAllowed(this, c.OtherLocation(this)) || m.IsAllowed(this, this))); if (!suitableMissions.Any()) { suitableMissions = unusedMissions; } - LocationConnection connection = null; - MissionPrefab missionPrefab = suitableMissions.GetRandom(); - var mission = InstantiateMission(missionPrefab, ref connection); + MissionPrefab missionPrefab = ToolBox.SelectWeightedRandom(suitableMissions.ToList(), suitableMissions.Select(m => (float)m.Commonness).ToList(), Rand.RandSync.Unsynced); + var mission = InstantiateMission(missionPrefab, out LocationConnection connection); //don't allow duplicate missions in the same connection if (AvailableMissions.Any(m => m.Prefab == missionPrefab && m.Locations.Contains(mission.Locations[0]) && m.Locations.Contains(mission.Locations[1]))) { @@ -360,8 +494,14 @@ namespace Barotrauma return null; } - private Mission InstantiateMission(MissionPrefab prefab, ref LocationConnection connection) + private Mission InstantiateMission(MissionPrefab prefab, out LocationConnection connection) { + if (prefab.IsAllowed(this, this)) + { + connection = null; + return InstantiateMission(prefab); + } + var suitableConnections = Connections.Where(c => prefab.IsAllowed(this, c.OtherLocation(this))); if (!suitableConnections.Any()) { @@ -371,21 +511,33 @@ namespace Barotrauma connection = ToolBox.SelectWeightedRandom( suitableConnections.ToList(), suitableConnections.Select(c => (c.Passed ? 1.0f : 5.0f) / Math.Max(availableMissions.Count(m => m.Locations.Contains(c.OtherLocation(this))), 1.0f)).ToList(), - Rand.RandSync.Unsynced); + Rand.RandSync.Unsynced); + return InstantiateMission(prefab, connection); + } + + private Mission InstantiateMission(MissionPrefab prefab, LocationConnection connection) + { Location destination = connection.OtherLocation(this); var mission = prefab.Instantiate(new Location[] { this, destination }); mission.AdjustLevelData(connection.LevelData); return mission; } + private Mission InstantiateMission(MissionPrefab prefab) + { + var mission = prefab.Instantiate(new Location[] { this, this }); + mission.AdjustLevelData(LevelData); + return mission; + } + public void InstantiateLoadedMissions(Map map) { availableMissions.Clear(); if (loadedMissions == null || loadedMissions.None()) { return; } foreach (LoadedMission loadedMission in loadedMissions) { - Location destination = null; + Location destination; if (loadedMission.DestinationIndex >= 0 && loadedMission.DestinationIndex < map.Locations.Count) { destination = map.Locations[loadedMission.DestinationIndex]; @@ -410,6 +562,23 @@ namespace Barotrauma SelectedMissionIndex = -1; } + public bool HasOutpost() + { + if (!Type.HasOutpost) { return false; } + + return !IsCriticallyRadiated(); + } + + public bool IsCriticallyRadiated() + { + if (GameMain.GameSession is { Campaign: { Map: { } map } }) + { + return TurnsInRadiation > map.Radiation.Params.CriticalRadiationThreshold; + } + + return false; + } + public IEnumerable GetMissionsInConnection(LocationConnection connection) { System.Diagnostics.Debug.Assert(Connections.Contains(connection)); @@ -456,6 +625,61 @@ 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; + } + } + } + + public bool IsRadiated() => GameMain.GameSession is { Campaign: { Map: { Radiation: { Enabled: true } radiation } } } && radiation.Contains(this); + private List CreateStoreStock() { var stock = new List(); @@ -463,31 +687,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 +747,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 +818,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 +847,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 +868,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 +893,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 +987,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,17 +1010,42 @@ namespace Barotrauma new XAttribute("discovered", Discovered), new XAttribute("position", XMLExtensions.Vector2ToString(MapPosition)), new XAttribute("pricemultiplier", PriceMultiplier), - new XAttribute("mechanicalpricemultipler", MechanicalPriceMultiplier)); - if (ProximityTime.Length > 0 && ProximityTime.Any(t => t > 0)) - { - locationElement.Add(new XAttribute("proximitytime", string.Join(',', ProximityTime.Select(i => i.ToString())))); - } + new XAttribute("isgatebetweenbiomes", IsGateBetweenBiomes), + new XAttribute("mechanicalpricemultipler", MechanicalPriceMultiplier), + new XAttribute("timesincelasttypechange", TimeSinceLastTypeChange), + new XAttribute(nameof(TurnsInRadiation).ToLower(), TurnsInRadiation)); LevelData.Save(locationElement); - if (TypeChangeTimer > 0) + for (int i = 0; i < Type.CanChangeTo.Count; i++) { - locationElement.Add(new XAttribute("changetimer", TypeChangeTimer)); + for (int j = 0; j < Type.CanChangeTo[i].Requirements.Count; j++) + { + if (ProximityTimer.ContainsKey(Type.CanChangeTo[i].Requirements[j])) + { + locationElement.Add(new XAttribute("proximitytimer" + i + "-" + j, ProximityTimer[Type.CanChangeTo[i].Requirements[j]])); + } + } } + + if (PendingLocationTypeChange.HasValue) + { + var changeElement = new XElement("pendinglocationtypechange", new XAttribute("timer", PendingLocationTypeChange.Value.delay)); + if (PendingLocationTypeChange.Value.parentMission != null) + { + changeElement.Add(new XAttribute("missionidentifier", PendingLocationTypeChange.Value.parentMission.Identifier)); + } + else + { + changeElement.Add(new XAttribute("index", Type.CanChangeTo.IndexOf(PendingLocationTypeChange.Value.typeChange))); + } + locationElement.Add(changeElement); + } + + if (LocationTypeChangeCooldown > 0) + { + locationElement.Add(new XAttribute("locationtypechangecooldown", LocationTypeChangeCooldown)); + } + if (takenItems.Any()) { locationElement.Add(new XAttribute( @@ -719,7 +1059,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; } @@ -727,6 +1071,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); } @@ -735,7 +1102,7 @@ namespace Barotrauma var missionsElement = new XElement("missions"); foreach (Mission mission in missions) { - var location = mission.Locations.FirstOrDefault(l => l != this); + var location = mission.Locations.All(l => l == this) ? this : mission.Locations.FirstOrDefault(l => l != this); var i = map.Locations.IndexOf(location); missionsElement.Add(new XElement("mission", new XAttribute("prefabid", mission.Prefab.Identifier), @@ -750,40 +1117,6 @@ namespace Barotrauma return locationElement; } - public int Distance(Location other, int maxRecursionDepth, int currRecursionDepth = 0) - { - if (currRecursionDepth >= maxRecursionDepth) { return -1; } - if (other == this) { return 0; } - int minDist = -1; - foreach (Location connected in Connections.Select(c => c.Locations.First(l => l != this))) - { - int dist = connected.Distance(other, maxRecursionDepth, currRecursionDepth+1); - if (dist >= 0) - { - if (minDist < 0 || dist < minDist) { minDist = dist; } - } - } - return minDist; - } - - public void DetermineProximityTime(Location currentLocation) - { - int dist = Distance(currentLocation, Type.CanChangeTo.Select(cct => cct.RequiredProximityForProbabilityIncrease).Max()); - for (int i=0;i 5) { ProximityTime[i] = 5; } - } - else - { - ProximityTime[i]--; - if (ProximityTime[i] < 0) { ProximityTime[i] = 0; } - } - } - } - public void Remove() { RemoveProjSpecific(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationConnection.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationConnection.cs index d2c0cecd3..d27827f70 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 @@ -31,8 +32,31 @@ namespace Barotrauma private set; } + private readonly List availableMissions = new List(); + public IEnumerable AvailableMissions + { + get + { + availableMissions.RemoveAll(m => m.Completed || (m.Failed && m.Prefab.AllowRetry)); + return availableMissions; + } + } + 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/LocationType.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs index cb91b64a3..c7dfceca6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs @@ -13,17 +13,12 @@ namespace Barotrauma class LocationType { public static readonly List List = new List(); - - private readonly List nameFormats; private readonly List names; - - private readonly Sprite symbolSprite; - private readonly List portraits = new List(); // - private List> hireableJobs; - private float totalHireableWeight; + private readonly List> hireableJobs; + private readonly float totalHireableWeight; public Dictionary CommonnessPerZone = new Dictionary(); @@ -34,16 +29,20 @@ namespace Barotrauma public readonly List CanChangeTo = new List(); + public readonly List MissionIdentifiers = new List(); + public readonly List MissionTags = new List(); + + public readonly List HideEntitySubcategories = new List(); + + public bool IsEnterable { get; private set; } + public bool UseInMainMenu { get; private set; } - - public List NameFormats - { - get { return nameFormats; } - } + + public List NameFormats { get; private set; } public bool HasHireableCharacters { @@ -56,10 +55,7 @@ namespace Barotrauma private set; } - public Sprite Sprite - { - get { return symbolSprite; } - } + public Sprite Sprite { get; private set; } public Color SpriteColor { @@ -79,9 +75,15 @@ namespace Barotrauma BeaconStationChance = element.GetAttributeFloat("beaconstationchance", 0.0f); - nameFormats = TextManager.GetAll("LocationNameFormat." + Identifier); + NameFormats = TextManager.GetAll("LocationNameFormat." + Identifier); UseInMainMenu = element.GetAttributeBool("useinmainmenu", false); HasOutpost = element.GetAttributeBool("hasoutpost", true); + IsEnterable = element.GetAttributeBool("isenterable", HasOutpost); + + MissionIdentifiers = element.GetAttributeStringArray("missionidentifiers", new string[0]).ToList(); + MissionTags = element.GetAttributeStringArray("missiontags", new string[0]).ToList(); + + HideEntitySubcategories = element.GetAttributeStringArray("hideentitysubcategories", new string[0]).ToList(); string nameFile = element.GetAttributeString("namefile", "Content/Map/locationNames.txt"); try @@ -135,11 +137,11 @@ namespace Barotrauma hireableJobs.Add(hireableJob); break; case "symbol": - symbolSprite = new Sprite(subElement, lazyLoad: true); + Sprite = new Sprite(subElement, lazyLoad: true); SpriteColor = subElement.GetAttributeColor("color", Color.White); break; case "changeto": - CanChangeTo.Add(new LocationTypeChange(Identifier, subElement)); + CanChangeTo.Add(new LocationTypeChange(Identifier, subElement, requireChangeMessages: true)); break; case "portrait": var portrait = new Sprite(subElement, lazyLoad: true); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs index d5ad202d0..4f7c3424c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; using System.Linq; using System.Xml.Linq; @@ -6,41 +8,268 @@ namespace Barotrauma { class LocationTypeChange { + public class Requirement + { + public enum FunctionType + { + Add, + Multiply + } + + public readonly FunctionType Function; + + /// + /// 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; + + /// + /// Does there need to be a beacon station within RequiredProximity + /// + public readonly bool RequireBeaconStation; + + /// + /// Does there need to be hunting grounds within RequiredProximity + /// + public readonly bool RequireHuntingGrounds; + + public Requirement(XElement element, LocationTypeChange change) + { + 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); + RequireBeaconStation = element.GetAttributeBool("requirebeaconstation", false); + RequireHuntingGrounds = element.GetAttributeBool("requirehuntinggrounds", false); + + string functionStr = element.GetAttributeString("function", "Add"); + if (!Enum.TryParse(functionStr, ignoreCase: true, out Function)) + { + DebugConsole.ThrowError( + $"Invalid location type change in location type \"{change.CurrentType}\". " + + $"\"{functionStr}\" is not a valid function."); + } + + Probability = element.GetAttributeFloat("probability", 1.0f); + + if (RequiredProximityForProbabilityIncrease > 0 || ProximityProbabilityIncrease > 0.0f) + { + if (!RequiredLocations.Any() && !RequireBeaconStation && !RequireHuntingGrounds) + { + DebugConsole.AddWarning( + $"Invalid location type change in location type \"{change.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 \"{change.CurrentType}\". " + + "Probability is configured to increase when near some other type of location, but the base probability is already 100%"); + } + } + } + + public bool MatchesLocation(Location location) + { + return RequiredLocations.Contains(location.Type.Identifier) && !location.IsCriticallyRadiated(); + } + + public bool AnyWithinDistance(Location location, int maxDistance, int currentDistance = 0, HashSet checkedLocations = null) + { + if (currentDistance > maxDistance) { return false; } + if (currentDistance > 0 && MatchesLocation(location)) { return true; } + + checkedLocations ??= new HashSet(); + checkedLocations.Add(location); + + foreach (var connection in location.Connections) + { + if (RequireBeaconStation && connection.LevelData.HasBeaconStation && connection.LevelData.IsBeaconActive) + { + return true; + } + if (RequireHuntingGrounds && connection.LevelData.HasHuntingGrounds) + { + return true; + } + + var otherLocation = connection.OtherLocation(location); + if (!checkedLocations.Contains(otherLocation)) + { + if (AnyWithinDistance(otherLocation, maxDistance, currentDistance + 1, checkedLocations)) { return true; } + } + } + + return false; + } + } + + public readonly string CurrentType; + public readonly string ChangeToType; + /// + /// Base probability per turn for the location to change if near one of the RequiredLocations + /// public readonly float Probability; - public readonly int RequiredDuration; - public readonly float ProximityProbabilityIncrease; - public readonly int RequiredProximityForProbabilityIncrease; + public readonly bool RequireDiscovered; + + public List Requirements = new List(); 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'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 LocationTypeChange(string currentType, XElement element) + /// + /// The location can't change it's type for this many turns after this location type changes occurs + /// + public readonly int CooldownAfterChange; + + public readonly Point RequiredDurationRange; + + public LocationTypeChange(string currentType, XElement element, bool requireChangeMessages, float defaultProbability = 0.0f) { - ChangeToType = element.GetAttributeString("type", ""); - Probability = element.GetAttributeFloat("probability", 1.0f); - RequiredDuration = element.GetAttributeInt("requiredduration", 0); + CurrentType = currentType; + ChangeToType = element.GetAttributeString("type", element.GetAttributeString("to", "")); - ProximityProbabilityIncrease = element.GetAttributeFloat("proximityprobabilityincrease", 0.0f); - RequiredProximityForProbabilityIncrease = element.GetAttributeInt("requiredproximityforprobabilityincrease", 0); + RequireDiscovered = element.GetAttributeBool("requirediscovered", false); 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); + + Probability = element.GetAttributeFloat("probability", defaultProbability); + + CooldownAfterChange = Math.Max(element.GetAttributeInt("cooldownafterchange", 0), 0); + + //backwards compatibility + if (element.Attribute("requiredlocations") != null) + { + Requirements.Add(new Requirement(element, this)); + } + + //backwards compatibility + if (element.Attribute("requiredduration") != null) + { + RequiredDurationRange = new Point(element.GetAttributeInt("requiredduration", 0)); + } string messageTag = element.GetAttributeString("messagetag", "LocationChange." + currentType + ".ChangeTo." + ChangeToType); Messages = TextManager.GetAll(messageTag); if (Messages == null) { - DebugConsole.ThrowError("No messages defined for the location type change " + currentType + " -> " + ChangeToType); + if (requireChangeMessages) + { + DebugConsole.ThrowError("No messages defined for the location type change " + currentType + " -> " + ChangeToType); + } + Messages = new List(); } + + foreach (XElement subElement in element.Elements()) + { + if (subElement.Name.ToString().Equals("requirement", StringComparison.OrdinalIgnoreCase)) + { + Requirements.Add(new Requirement(subElement, this)); + } + } + } + + public float DetermineProbability(Location location) + { + if (RequireDiscovered && !location.Discovered) { return 0.0f; } + if (location.IsCriticallyRadiated()) { return 0.0f; } + if (location.LocationTypeChangeCooldown > 0) { return 0.0f; } + if (location.IsGateBetweenBiomes) { return 0.0f; } + + if (DisallowedAdjacentLocations.Any() && + AnyWithinDistance(location, DisallowedProximity, (otherLocation) => { return DisallowedAdjacentLocations.Contains(otherLocation.Type.Identifier); })) + { + return 0.0f; + } + + float probability = Probability; + foreach (Requirement requirement in Requirements) + { + if (requirement.AnyWithinDistance(location, requirement.RequiredProximity)) + { + if (requirement.Function == Requirement.FunctionType.Add) + { + probability += requirement.Probability; + } + else + { + probability *= requirement.Probability; + } + } + + if (location.ProximityTimer.ContainsKey(requirement)) + { + if (requirement.AnyWithinDistance(location, requirement.RequiredProximityForProbabilityIncrease)) + { + if (requirement.Function == Requirement.FunctionType.Add) + { + probability += requirement.ProximityProbabilityIncrease * location.ProximityTimer[requirement]; + } + else + { + probability *= requirement.ProximityProbabilityIncrease * location.ProximityTimer[requirement]; + } + } + } + } + + return probability; + } + + private bool AnyWithinDistance(Location location, int maxDistance, Func predicate, int currentDistance = 0, HashSet checkedLocations = null) + { + if (currentDistance > maxDistance) { return false; } + if (currentDistance > 0 && predicate(location)) { return true; } + + checkedLocations ??= new HashSet(); + checkedLocations.Add(location); + + foreach (var connection in location.Connections) + { + var otherLocation = connection.OtherLocation(location); + if (!checkedLocations.Contains(otherLocation)) + { + if (AnyWithinDistance(otherLocation, maxDistance, predicate, currentDistance + 1, checkedLocations)) { return true; } + } + } + + return false; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index ab81bff7b..5356ffd92 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -1,4 +1,5 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; @@ -15,8 +16,8 @@ namespace Barotrauma private Location furthestDiscoveredLocation; - private int Width => generationParams.Width; - private int Height => generationParams.Height; + public int Width => generationParams.Width; + public int Height => generationParams.Height; public Action OnLocationSelected; /// @@ -56,11 +57,14 @@ namespace Barotrauma public List Connections { get; private set; } + public Radiation Radiation; + public Map() { generationParams = MapGenerationParams.Instance; Locations = new List(); Connections = new List(); + Radiation = new Radiation(this, generationParams.RadiationParams); } /// @@ -69,6 +73,7 @@ namespace Barotrauma private Map(CampaignMode campaign, XElement element) : this() { Seed = element.GetAttributeString("seed", "a"); + Rand.SetSyncedSeed(ToolBox.StringToInt(Seed)); foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -80,11 +85,17 @@ namespace Barotrauma Locations.Add(null); } Locations[i] = new Location(subElement); - Locations[i].Reputation ??= new Reputation(campaign.CampaignMetadata, $"location.{i}", -100, 100, Rand.Range(-10, 10, Rand.RandSync.Server)); + break; + case "radiation": + Radiation = new Radiation(this, generationParams.RadiationParams, subElement); break; } } System.Diagnostics.Debug.Assert(!Locations.Contains(null)); + for (int i = 0; i < Locations.Count; i++) + { + Locations[i].Reputation ??= new Reputation(campaign.CampaignMetadata, $"location.{i}", -100, 100, Rand.Range(-10, 10, Rand.RandSync.Server)); + } foreach (XElement subElement in element.Elements()) { @@ -92,6 +103,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), @@ -181,8 +193,8 @@ namespace Barotrauma } System.Diagnostics.Debug.Assert(StartLocation != null, "Start location not assigned after level generation."); - CurrentLocation.CreateStore(); CurrentLocation.Discovered = true; + CurrentLocation.CreateStore(); InitProjectSpecific(); } @@ -243,21 +255,21 @@ 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 }; int positionIndex = Rand.Int(1, Rand.RandSync.Server); Vector2 position = points[positionIndex]; - if (newLocations[1 - i] != null && newLocations[1 - i].MapPosition == position) position = points[1 - positionIndex]; - int zone = MathHelper.Clamp((int)Math.Floor(position.X / zoneWidth) + 1, 1, generationParams.DifficultyZones); - newLocations[i] = Location.CreateRandom(position, zone, Rand.GetRNG(Rand.RandSync.Server), requireOutpost: false, Locations); + if (newLocations[1 - i] != null && newLocations[1 - i].MapPosition == position) { position = points[1 - positionIndex]; } + int zone = GetZoneIndex(position.X); + newLocations[i] = Location.CreateRandom(position, zone, Rand.GetRNG(Rand.RandSync.Server), requireOutpost: false, existingLocations: Locations); Locations.Add(newLocations[i]); } var newConnection = new LocationConnection(newLocations[0], newLocations[1]); - Connections.Add(newConnection); + Connections.Add(newConnection); } //remove connections that are too short @@ -316,7 +328,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); @@ -337,6 +357,56 @@ namespace Barotrauma } } + LocationConnection[] connectionsBetweenZones = new LocationConnection[generationParams.DifficultyZones]; + foreach (var connection in Connections) + { + int zone1 = GetZoneIndex(connection.Locations[0].MapPosition.X); + int zone2 = GetZoneIndex(connection.Locations[1].MapPosition.X); + if (zone1 == zone2) { continue; } + if (zone1 > zone2) + { + int temp = zone2; + zone2 = zone1; + zone1 = temp; + } + + if (connectionsBetweenZones[zone1] == null) + { + connectionsBetweenZones[zone1] = connection; + } + else + { + if (Math.Abs(connection.CenterPos.Y - Height / 2) < Math.Abs(connectionsBetweenZones[zone1].CenterPos.Y - Height / 2)) + { + connectionsBetweenZones[zone1] = connection; + } + } + } + + for (int i = Connections.Count - 1; i >= 0; i--) + { + int zone1 = GetZoneIndex(Connections[i].Locations[0].MapPosition.X); + int zone2 = GetZoneIndex(Connections[i].Locations[1].MapPosition.X); + if (zone1 == zone2) { continue; } + + if (!connectionsBetweenZones.Contains(Connections[i])) + { + Connections.RemoveAt(i); + } + else + { + var leftMostLocation = + Connections[i].Locations[0].MapPosition.X < Connections[i].Locations[1].MapPosition.X ? + Connections[i].Locations[0] : + Connections[i].Locations[1]; + if (!leftMostLocation.Type.HasOutpost) + { + leftMostLocation.ChangeType(LocationType.List.First(lt => lt.HasOutpost)); + } + leftMostLocation.IsGateBetweenBiomes = true; + } + } + foreach (Location location in Locations) { for (int i = location.Connections.Count - 1; i >= 0; i--) @@ -359,6 +429,14 @@ namespace Barotrauma foreach (Location location in Locations) { location.LevelData = new LevelData(location); + if (location.Type.MissionIdentifiers.Any()) + { + location.UnlockMissionByIdentifier(location.Type.MissionIdentifiers.GetRandom()); + } + if (location.Type.MissionTags.Any()) + { + location.UnlockMissionByTag(location.Type.MissionTags.GetRandom()); + } } foreach (LocationConnection connection in Connections) { @@ -368,6 +446,12 @@ namespace Barotrauma partial void GenerateLocationConnectionVisuals(); + private int GetZoneIndex(float xPos) + { + float zoneWidth = Width / generationParams.DifficultyZones; + return MathHelper.Clamp((int)Math.Floor(xPos / zoneWidth) + 1, 1, generationParams.DifficultyZones); + } + public Biome GetBiome(Vector2 mapPos) { return GetBiome(mapPos.X); @@ -544,6 +628,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) @@ -618,7 +708,6 @@ namespace Barotrauma public void SelectMission(int missionIndex) { - if (SelectedConnection == null) { return; } if (CurrentLocation == null) { string errorMsg = "Failed to select a mission (current location not set)."; @@ -628,11 +717,18 @@ namespace Barotrauma } CurrentLocation.SelectedMissionIndex = missionIndex; - //the destination must be the same as the destination of the mission - if (CurrentLocation.SelectedMission != null && - CurrentLocation.SelectedMission.Locations[1] != SelectedLocation) + if (CurrentLocation.SelectedMission == null) { return; } + + if (CurrentLocation.SelectedMission.Locations[0] != CurrentLocation || + CurrentLocation.SelectedMission.Locations[1] != CurrentLocation) { - CurrentLocation.SelectedMissionIndex = -1; + if (SelectedConnection == null) { return; } + //the destination must be the same as the destination of the mission + if (CurrentLocation.SelectedMission != null && + CurrentLocation.SelectedMission.Locations[1] != SelectedLocation) + { + CurrentLocation.SelectedMissionIndex = -1; + } } OnMissionSelected?.Invoke(SelectedConnection, CurrentLocation.SelectedMission); @@ -668,89 +764,116 @@ namespace Barotrauma { ProgressWorld(); } + + Radiation.OnStep(steps); } private void ProgressWorld() { foreach (Location location in Locations) { - if (!location.Discovered) { continue; } - - 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) + { + 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]; - //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(); } + } + } - //select a random type change - if (Rand.Range(0.0f, 1.0f) < readyTypeChanges.Sum(i => cct[i].Probability + (cct[i].ProximityProbabilityIncrease * (float)location.ProximityTime[i]))) + private void ProgressLocationTypeChanges(Location location) + { + location.TimeSinceLastTypeChange++; + location.LocationTypeChangeCooldown--; + + if (location.PendingLocationTypeChange != null) + { + if (location.PendingLocationTypeChange.Value.typeChange.DetermineProbability(location) <= 0.0f) { - var selectedTypeChangeIndex = - ToolBox.SelectWeightedRandom( - readyTypeChanges, - readyTypeChanges.Select(i => cct[i].Probability + (cct[i].ProximityProbabilityIncrease * (float)location.ProximityTime[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; - break; - } - } - - 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 = + (location.PendingLocationTypeChange.Value.typeChange, + location.PendingLocationTypeChange.Value.delay - 1, + location.PendingLocationTypeChange.Value.parentMission); + if (location.PendingLocationTypeChange.Value.delay <= 0) + { + ChangeLocationType(location, location.PendingLocationTypeChange.Value.typeChange); + } + 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 = + (selectedTypeChange, + Rand.Range(selectedTypeChange.RequiredDurationRange.X, selectedTypeChange.RequiredDurationRange.Y), + null); + } + else + { + ChangeLocationType(location, selectedTypeChange); + } + return; + } + } + + foreach (LocationTypeChange typeChange in location.Type.CanChangeTo) + { + foreach (var requirement in typeChange.Requirements) + { + if (requirement.AnyWithinDistance(location, requirement.RequiredProximityForProbabilityIncrease)) + { + if (!location.ProximityTimer.ContainsKey(requirement)) { location.ProximityTimer[requirement] = 0; } + location.ProximityTimer[requirement] += 1; + } + else + { + location.ProximityTimer.Remove(requirement); + } + } } } @@ -799,7 +922,22 @@ 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); + foreach (var requirement in change.Requirements) + { + location.ProximityTimer.Remove(requirement); + } + location.TimeSinceLastTypeChange = 0; + location.LocationTypeChangeCooldown = change.CooldownAfterChange; + location.PendingLocationTypeChange = null; + } + + partial void ChangeLocationTypeProjSpecific(Location location, string prevName, LocationTypeChange change); + partial void ClearAnimQueue(); /// @@ -835,8 +973,15 @@ 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++) + { + for (int j = 0; j < location.Type.CanChangeTo[i].Requirements.Count; j++) + { + location.ProximityTimer.Add(location.Type.CanChangeTo[i].Requirements[j], subElement.GetAttributeInt("changetimer" + i + "-" + j, 0)); + } + } + location.LoadLocationTypeChange(subElement); location.Discovered = subElement.GetAttributeBool("discovered", false); if (location.Discovered) { @@ -849,7 +994,6 @@ namespace Barotrauma } } - string locationType = subElement.GetAttributeString("type", ""); string prevLocationName = location.Name; LocationType prevLocationType = location.Type; @@ -860,15 +1004,22 @@ 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); Connections[connectionIndex].Passed = subElement.GetAttributeBool("passed", false); break; + case "radiation": + Radiation = new Radiation(this, generationParams.RadiationParams, subElement); + break; } } @@ -935,6 +1086,8 @@ namespace Barotrauma mapElement.Add(connectionElement); } + mapElement.Add(Radiation.Save()); + element.Add(mapElement); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/MapGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/MapGenerationParams.cs index b745dff26..6e9194dd6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/MapGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/MapGenerationParams.cs @@ -125,6 +125,8 @@ namespace Barotrauma get; private set; } + public RadiationParams RadiationParams; + public static void Init() { @@ -238,6 +240,9 @@ namespace Barotrauma TypeChangeIcon = new Sprite(subElement); break; #endif + case "radiationparams": + RadiationParams = new RadiationParams(subElement); + break; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Radiation.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Radiation.cs new file mode 100644 index 000000000..332223772 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Radiation.cs @@ -0,0 +1,140 @@ +#nullable enable +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + internal partial class Radiation : ISerializableEntity + { + public string Name => nameof(Radiation); + + [Serialize(defaultValue: 0f, isSaveable: true)] + public float Amount { get; set; } + + public Dictionary SerializableProperties { get; } + + public readonly Map Map; + public readonly RadiationParams Params; + + private float radiationTimer; + + private float increasedAmount; + private float lastIncrease; + + public bool Enabled = true; + + public Radiation(Map map, RadiationParams radiationParams, XElement? element = null) + { + SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + Map = map; + Params = radiationParams; + radiationTimer = Params.RadiationDamageDelay; + if (element == null) + { + Amount = Params.StartingRadiation; + } + } + + /// + /// Advances the progress of the radiation. + /// + /// + public void OnStep(float steps = 1) + { + if (!Enabled) { return; } + if (steps <= 0) { return; } + + IncreaseRadiation(Params.RadiationStep * steps); + + int amountOfOutposts = Map.Locations.Count(location => location.Type.HasOutpost && !location.IsCriticallyRadiated()); + + foreach (Location location in Map.Locations.Where(Contains)) + { + if (amountOfOutposts <= Params.MinimumOutpostAmount) { break; } + + if (Map.CurrentLocation is { } currLocation) + { + // Don't advance on nearby locations to avoid buggy behavior + if (currLocation == location || currLocation.Connections.Any(lc => lc.OtherLocation(currLocation) == location)) { continue; } + } + + bool wasCritical = location.IsCriticallyRadiated(); + + location.TurnsInRadiation++; + + if (location.Type.HasOutpost && !wasCritical && location.IsCriticallyRadiated()) + { + amountOfOutposts--; + } + } + } + + public void IncreaseRadiation(float amount) + { + Amount += amount; + increasedAmount = lastIncrease = amount; + } + + public void UpdateRadiation(float deltaTime) + { + if (!(GameMain.GameSession?.IsCurrentLocationRadiated() ?? false)) { return; } + + if (GameMain.NetworkMember is { IsClient: true }) { return; } + + if (radiationTimer > 0) + { + radiationTimer -= deltaTime; + return; + } + + radiationTimer = Params.RadiationDamageDelay; + + foreach (Character character in Character.CharacterList) + { + if (character.IsDead || character.Removed || !(character.CharacterHealth is { } health)) { continue; } + + if (IsEntityRadiated(character)) + { + health.ApplyAffliction(null, new Affliction(AfflictionPrefab.RadiationSickness, Params.RadiationDamageAmount)); + } + } + } + + public bool Contains(Location location) + { + return Contains(location.MapPosition); + } + + public bool Contains(Vector2 pos) + { + return pos.X < Amount; + } + + public bool IsEntityRadiated(Entity entity) + { + if (!Enabled) { return false; } + if (Level.Loaded is { Type: LevelData.LevelType.LocationConnection, StartLocation: { } startLocation, EndLocation: { } endLocation } level) + { + if (Contains(startLocation) && Contains(endLocation)) { return true; } + + float distance = MathHelper.Clamp((entity.WorldPosition.X - level.StartPosition.X) / (level.EndPosition.X - level.StartPosition.X), 0.0f, 1.0f); + var (startX, startY) = startLocation.MapPosition; + var (endX, endY) = endLocation.MapPosition; + Vector2 mapPos = new Vector2(startX + (endX - startX), startY + (endY - startY)) * distance; + + return Contains(mapPos); + } + + return false; + } + + public XElement Save() + { + XElement element = new XElement(nameof(Radiation)); + SerializableProperty.SerializeProperties(this, element); + return element; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/RadiationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/RadiationParams.cs new file mode 100644 index 000000000..b806a2f69 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/RadiationParams.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Barotrauma +{ + internal class RadiationParams: ISerializableEntity + { + public string Name => nameof(RadiationParams); + public Dictionary SerializableProperties { get; } + + [Serialize(defaultValue: -100f, isSaveable: false, "How much radiation the world starts with.")] + public float StartingRadiation { get; set; } + + [Serialize(defaultValue: 100f, isSaveable: false, "How much radiation is added on each step.")] + public float RadiationStep { get; set; } + + [Serialize(defaultValue: 10, isSaveable: false, "How many turns in radiation does it take for an outpost to be removed from the map.")] + public int CriticalRadiationThreshold { get; set; } + + [Serialize(defaultValue: 3, isSaveable: false, "Minimum amount of outposts in the level that cannot be removed due to radiation.")] + public int MinimumOutpostAmount { get; set; } + + [Serialize(defaultValue: 3f, isSaveable: false, "How fast the radiation increase animation goes.")] + public float AnimationSpeed { get; set; } + + [Serialize(defaultValue: 10f, isSaveable: false, "How long it takes to apply more radiation damage while in a radiated zone.")] + public float RadiationDamageDelay { get; set; } + + [Serialize(defaultValue: 1f, isSaveable: false, "How much is the radiation affliction increased by while in a radiated zone.")] + public float RadiationDamageAmount { get; set; } + + public RadiationParams(XElement element) + { + SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + } + } +} \ No newline at end of file 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/MapEntityPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs index a3c0bc602..93eedb775 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs @@ -3,14 +3,23 @@ using System; using System.Collections.Generic; using System.Linq; using System.Reflection; -using System.Xml.Linq; namespace Barotrauma { [Flags] enum MapEntityCategory { - Structure = 1, Decorative = 2, Machine = 4, Equipment = 8, Electrical = 16, Material = 32, Misc = 64, Alien = 128, Wrecked = 256, Thalamus = 512, ItemAssembly = 1024, Legacy = 2048 + Structure = 1, + Decorative = 2, + Machine = 4, + Equipment = 8, + Electrical = 16, + Material = 32, + Misc = 64, + Alien = 128, + Wrecked = 256, + ItemAssembly = 512, + Legacy = 1024 } abstract partial class MapEntityPrefab : IPrefab, IDisposable @@ -54,6 +63,7 @@ namespace Barotrauma //is it possible to stretch the entity horizontally/vertically [Serialize(false, false)] public bool ResizeHorizontal { get; protected set; } + [Serialize(false, false)] public bool ResizeVertical { get; protected set; } @@ -118,6 +128,9 @@ namespace Barotrauma [Serialize(false, false)] public bool HideInMenus { get; set; } + [Serialize("", false)] + public string Subcategory { get; set; } + [Serialize(false, false)] public bool Linkable { @@ -215,6 +228,11 @@ namespace Barotrauma return string.IsNullOrWhiteSpace(AllowedUpgrades) ? new string[0] : AllowedUpgrades.Split(","); } + public bool HasSubCategory(string subcategory) + { + return subcategory?.Equals(this.Subcategory, StringComparison.OrdinalIgnoreCase) ?? false; + } + protected virtual void CreateInstance(Rectangle rect) { if (constructor == null) return; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs index 99d83d7b2..e7f868b20 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs @@ -39,6 +39,34 @@ namespace Barotrauma set; } + [Serialize(false, isSaveable: true), Editable] + public bool AlwaysDestructible + { + get; + set; + } + + [Serialize(false, isSaveable: true), Editable] + public bool AlwaysRewireable + { + get; + set; + } + + [Serialize(false, isSaveable: true), Editable] + public bool AllowStealing + { + get; + set; + } + + [Serialize(true, isSaveable: true), Editable] + public bool SpawnCrewInsideOutpost + { + get; + set; + } + private readonly Dictionary moduleCounts = new Dictionary(); public IEnumerable> ModuleCounts diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index 998ad3cd3..20c8c4acf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs @@ -169,6 +169,7 @@ namespace Barotrauma Type = SubmarineType.Outpost }; generationFailed = false; + outpostInfo.OutpostGenerationParams = generationParams; sub = new Submarine(outpostInfo, loadEntities: loadEntities); sub.Info.OutpostGenerationParams = generationParams; if (!generationFailed) @@ -669,10 +670,15 @@ namespace Barotrauma if (availableModules.Count() == 0) { return null; } - var modulesSuitableForLocationType = - availableModules.Where(m => - !m.OutpostModuleInfo.AllowedLocationTypes.Any() || - m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier.ToLowerInvariant())); + //try to search for modules made specifically for this location type first + var modulesSuitableForLocationType = + availableModules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier.ToLowerInvariant())); + + //if not found, search for modules suitable for any location type + if (!modulesSuitableForLocationType.Any()) + { + modulesSuitableForLocationType = availableModules.Where(m => !m.OutpostModuleInfo.AllowedLocationTypes.Any()); + } if (!modulesSuitableForLocationType.Any()) { @@ -705,10 +711,15 @@ namespace Barotrauma if (availableModules.Count() == 0) { return null; } + //try to search for modules made specifically for this location type first var modulesSuitableForLocationType = - availableModules.Where(m => - !m.OutpostModuleInfo.AllowedLocationTypes.Any() || - m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier.ToLowerInvariant())); + availableModules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier.ToLowerInvariant())); + + //if not found, search for modules suitable for any location type + if (!modulesSuitableForLocationType.Any()) + { + modulesSuitableForLocationType = availableModules.Where(m => !m.OutpostModuleInfo.AllowedLocationTypes.Any()); + } if (!modulesSuitableForLocationType.Any()) { @@ -1381,15 +1392,14 @@ namespace Barotrauma Rand.SetSyncedSeed(ToolBox.StringToInt(characterInfo.Name)); ISpatialEntity gotoTarget = SpawnAction.GetSpawnPos(SpawnAction.SpawnLocationType.Outpost, SpawnType.Human, humanPrefab.GetModuleFlags(), humanPrefab.GetSpawnPointTags()); - if (gotoTarget == null) { 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,29 +1414,12 @@ 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 = !outpost.Info.OutpostGenerationParams.AllowStealing; } npc.GiveIdCardTags(gotoTarget as WayPoint); - if (npc.AIController is HumanAIController humanAI) - { - var idleObjective = humanAI.ObjectiveManager.GetObjective(); - if (humanPrefab.CampaignInteractionType != CampaignMode.InteractionType.None) - { - idleObjective.Behavior = AIObjectiveIdle.BehaviorType.StayInHull; - idleObjective.TargetHull = AIObjectiveGoTo.GetTargetHull(gotoTarget); - (GameMain.GameSession.GameMode as CampaignMode)?.AssignNPCMenuInteraction(npc, humanPrefab.CampaignInteractionType); - } - else - { - idleObjective.Behavior = humanPrefab.Behavior; - foreach (string moduleType in humanPrefab.PreferredOutpostModuleTypes) - { - idleObjective.PreferredOutpostModuleTypes.Add(moduleType); - } - } - } + humanPrefab.InitializeCharacter(npc, gotoTarget); } } } 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 95bb13bef..2b4fcedd4 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 @@ -144,10 +142,16 @@ namespace Barotrauma { get { - return Prefab.Body && !IsPlatform; + return Prefab.Body && !IsPlatform;// && HasDamage; } } + public bool HasDamage + { + get; + private set; + } + public StructurePrefab Prefab => prefab as StructurePrefab; public HashSet Tags @@ -356,7 +360,7 @@ namespace Barotrauma #endif } - public Structure(Rectangle rectangle, StructurePrefab sp, Submarine submarine, ushort id = Entity.NullEntityID) + public Structure(Rectangle rectangle, StructurePrefab sp, Submarine submarine, ushort id = Entity.NullEntityID, XElement element = null) : base(sp, submarine, id) { System.Diagnostics.Debug.Assert(rectangle.Width > 0 && rectangle.Height > 0); @@ -395,7 +399,6 @@ namespace Barotrauma StairDirection = Prefab.StairDirection; NoAITarget = Prefab.NoAITarget; - SerializableProperties = SerializableProperty.GetProperties(this); InitProjSpecific(); @@ -421,6 +424,8 @@ namespace Barotrauma } } + SerializableProperties = element != null ? SerializableProperty.DeserializeProperties(this, element) : SerializableProperty.GetProperties(this); + // Only add ai targets automatically to submarine/outpost walls if (aiTarget == null && HasBody && Tags.Contains("wall") && submarine != null && !submarine.Info.IsWreck && !NoAITarget) { @@ -632,7 +637,7 @@ namespace Barotrauma Vector2 bodyPos = WorldPosition + BodyOffset; - Vector2 transformedMousePos = MathUtils.RotatePointAroundTarget(position, bodyPos, MathHelper.ToDegrees(BodyRotation)); + Vector2 transformedMousePos = MathUtils.RotatePointAroundTarget(position, bodyPos, BodyRotation); return Math.Abs(transformedMousePos.X - bodyPos.X) < rectSize.X / 2.0f && @@ -836,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) { @@ -850,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); @@ -944,14 +949,14 @@ namespace Barotrauma return new AttackResult(damageAmount, null); } - private void SetDamage(int sectionIndex, float damage, Character attacker = null, bool createNetworkEvent = true) + public void SetDamage(int sectionIndex, float damage, Character attacker = null, bool createNetworkEvent = true) { if (Submarine != null && Submarine.GodMode || Indestructible) { return; } if (!Prefab.Body) { return; } if (!MathUtils.IsValid(damage)) { return; } damage = MathHelper.Clamp(damage, 0.0f, MaxHealth - Prefab.MinHealth); - + #if SERVER if (GameMain.Server != null && createNetworkEvent && damage != Sections[sectionIndex].damage) { @@ -1065,15 +1070,17 @@ namespace Barotrauma } float gapOpen = (damage / MaxHealth - LeakThreshold) * (1.0f / (1.0f - LeakThreshold)); - Sections[sectionIndex].gap.Open = gapOpen; + Sections[sectionIndex].gap.Open = gapOpen; } float damageDiff = damage - Sections[sectionIndex].damage; bool hadHole = SectionBodyDisabled(sectionIndex); Sections[sectionIndex].damage = MathHelper.Clamp(damage, 0.0f, MaxHealth); - + HasDamage = Sections.Any(s => s.damage > 0.0f); + if (attacker != null && damageDiff != 0.0f) { + HumanAIController.StructureDamaged(this, damageDiff, attacker); OnHealthChangedProjSpecific(attacker, damageDiff); if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) { @@ -1081,14 +1088,14 @@ namespace Barotrauma { attacker.Info.IncreaseSkillLevel("mechanical", -damageDiff * SkillSettings.Current.SkillIncreasePerRepairedStructureDamage / Math.Max(attacker.GetSkillLevel("mechanical"), 1.0f), - SectionPosition(sectionIndex, true)); + SectionPosition(sectionIndex)); } } } bool hasHole = SectionBodyDisabled(sectionIndex); - if (hadHole == hasHole) return; + if (hadHole == hasHole) { return; } UpdateSections(); } @@ -1283,18 +1290,17 @@ namespace Barotrauma } Rectangle rect = element.GetAttributeRect("rect", Rectangle.Empty); - Structure s = new Structure(rect, prefab, submarine, idRemap.GetOffsetId(element)) + Structure s = new Structure(rect, prefab, submarine, idRemap.GetOffsetId(element), element) { Submarine = submarine, }; - SerializableProperty.DeserializeProperties(s, element); - if (submarine?.Info.GameVersion != null) { SerializableProperty.UpgradeGameVersion(s, s.Prefab.ConfigElement, submarine.Info.GameVersion); } + bool hasDamage = false; foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -1311,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": @@ -1333,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) @@ -1347,6 +1355,11 @@ namespace Barotrauma s.NoAITarget = prefab.NoAITarget; } + if (hasDamage) + { + s.UpdateSections(); + } + return s; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs index 6afe488b6..d4e620846 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs @@ -360,7 +360,8 @@ namespace Barotrauma } } - if (!Enum.TryParse(element.GetAttributeString("category", "Structure"), true, out MapEntityCategory category)) + string categoryStr = element.GetAttributeString("category", "Structure"); + if (!Enum.TryParse(categoryStr, true, out MapEntityCategory category)) { category = MapEntityCategory.Structure; } @@ -419,6 +420,13 @@ namespace Barotrauma } } + //backwards compatibility + if (categoryStr.Equals("Thalamus", StringComparison.OrdinalIgnoreCase)) + { + sp.Category = MapEntityCategory.Wrecked; + sp.Subcategory = "Thalamus"; + } + if (string.IsNullOrEmpty(sp.identifier)) { DebugConsole.ThrowError( diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index 9f4601332..8b43bc834 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; } } @@ -304,7 +304,6 @@ namespace Barotrauma { if ((!anyHasTag || item.HasTag("ballast")) && item.GetComponent() is { } pump) { - if (pump.Infected) { continue; } pumps.Add(pump); } } @@ -312,11 +311,13 @@ namespace Barotrauma if (!pumps.Any()) { return; } Pump randomPump = pumps.GetRandom(Rand.RandSync.Unsynced); - randomPump.Infected = true; - randomPump.InfectIdentifier = identifier; + if (randomPump.IsOn && randomPump.HasPower && randomPump.FlowPercentage > 0 && randomPump.Item.Condition > 0.0f) + { + randomPump.InfectBallast(identifier); #if SERVER - randomPump.Item.CreateServerEvent(randomPump); + randomPump.Item.CreateServerEvent(randomPump); #endif + } } public void MakeWreck() @@ -324,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); @@ -553,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; } @@ -586,6 +587,7 @@ namespace Barotrauma { if (e is Item item) { + if (item.GetComponent() != null) { return false; } if (item.body != null && !item.body.Enabled) { return true; } } return false; @@ -598,6 +600,17 @@ namespace Barotrauma for (int i = 1; i < entities.Count; i++) { + if (entities[i] is Item item) + { + var turret = item.GetComponent(); + if (turret != null) + { + minX = Math.Min(minX, entities[i].Rect.X + turret.TransformedBarrelPos.X * 2f); + minY = Math.Min(minY, entities[i].Rect.Y - entities[i].Rect.Height - turret.TransformedBarrelPos.Y * 2f); + maxX = Math.Max(maxX, entities[i].Rect.Right + turret.TransformedBarrelPos.X * 2f); + maxY = Math.Max(maxY, entities[i].Rect.Y - turret.TransformedBarrelPos.Y * 2f); + } + } minX = Math.Min(minX, entities[i].Rect.X); minY = Math.Min(minY, entities[i].Rect.Y - entities[i].Rect.Height); maxX = Math.Max(maxX, entities[i].Rect.Right); @@ -1136,7 +1149,7 @@ namespace Barotrauma { if (ConnectedDockingPorts.TryGetValue(dockedSub, out DockingPort port)) { - port.Undock(); + port.Undock(applyEffects: false); continue; } } @@ -1277,11 +1290,7 @@ namespace Barotrauma HiddenSubPosition += Vector2.UnitY * (sub.Borders.Height + 5000.0f); } - IdOffset = 0; - foreach (MapEntity me in MapEntity.mapEntityList) - { - IdOffset = Math.Max(IdOffset, me.ID); - } + IdOffset = IdRemap.DetermineNewOffset(); List newEntities = new List(); if (loadEntities == null) @@ -1335,16 +1344,20 @@ namespace Barotrauma { ShowSonarMarker = false; PhysicsBody.FarseerBody.BodyType = BodyType.Static; - TeamID = Character.TeamType.FriendlyNPC; + TeamID = CharacterTeamType.FriendlyNPC; + + bool indestructible = + GameMain.NetworkMember != null && + !GameMain.NetworkMember.ServerSettings.DestructibleOutposts && + !(info.OutpostGenerationParams?.AlwaysDestructible ?? false); foreach (MapEntity me in MapEntity.mapEntityList) { if (me.Submarine != this) { continue; } if (me is Item item) { - item.SpawnedInOutpost = true; - if (item.GetComponent() != null && - (GameMain.NetworkMember != null && !GameMain.NetworkMember.ServerSettings.DestructibleOutposts)) + item.SpawnedInOutpost = !info.OutpostGenerationParams.AllowStealing; + if (item.GetComponent() != null && indestructible) { item.Indestructible = true; } @@ -1353,7 +1366,10 @@ namespace Barotrauma if (ic is ConnectionPanel connectionPanel) { //prevent rewiring - connectionPanel.Locked = true; + if (!info.OutpostGenerationParams.AlwaysRewireable) + { + connectionPanel.Locked = true; + } } else if (ic is Holdable holdable && holdable.Attached && item.GetComponent() == null) { @@ -1366,9 +1382,9 @@ namespace Barotrauma } } } - else if (me is Structure structure && structure.Prefab.IndestructibleInOutposts) + else if (me is Structure structure && structure.Prefab.IndestructibleInOutposts && indestructible) { - structure.Indestructible = GameMain.NetworkMember != null && !GameMain.NetworkMember.ServerSettings.DestructibleOutposts; + structure.Indestructible = true; } } } @@ -1498,7 +1514,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 { @@ -1520,6 +1548,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); } @@ -1694,6 +1726,7 @@ namespace Barotrauma } } } + node.Waypoint.FindHull(); } } @@ -1708,6 +1741,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 dfbbb22fd..43efe44fc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs @@ -1,4 +1,5 @@ using Barotrauma.Extensions; +using Barotrauma.Items.Components; using Barotrauma.Networking; using FarseerPhysics; using FarseerPhysics.Collision; @@ -446,21 +447,24 @@ namespace Barotrauma private void UpdateDepthDamage(float deltaTime) { #if CLIENT - if (GameMain.GameSession.GameMode is TestGameMode) { return; } + if (GameMain.GameSession?.GameMode is TestGameMode) { return; } #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; - Explosion.RangedStructureDamage(wall.WorldPosition, 100.0f, pastCrushDepth * 0.1f); + 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) { GameMain.GameScreen.Cam.Shake = Math.Max(GameMain.GameScreen.Cam.Shake, Math.Min(pastCrushDepth * 0.001f, 50.0f)); @@ -555,19 +559,29 @@ namespace Barotrauma { if (limb?.body?.FarseerBody == null || limb.character == null) { return; } - if (limb.Mass > MinImpactLimbMass) + float impactMass = limb.Mass; + var enemyAI = limb.character.AIController as EnemyAIController; + float attackMultiplier = 1.0f; + if (enemyAI?.ActiveAttack != null) + { + impactMass = Math.Max(Math.Max(limb.Mass, limb.character.AnimController.MainLimb.Mass), limb.character.AnimController.Collider.Mass); + attackMultiplier = enemyAI.ActiveAttack.SubmarineImpactMultiplier; + } + + if (impactMass * attackMultiplier > MinImpactLimbMass) { Vector2 normal = Vector2.DistanceSquared(Body.SimPosition, limb.SimPosition) < 0.0001f ? Vector2.UnitY : Vector2.Normalize(Body.SimPosition - limb.SimPosition); - float impact = Math.Min(Vector2.Dot(collision.Velocity, -normal), 50.0f) * Math.Min(limb.Mass / 100.0f, 1); + float impact = Math.Min(Vector2.Dot(collision.Velocity, -normal), 50.0f) * Math.Min(impactMass / 300.0f, 1); + impact *= attackMultiplier; - ApplyImpact(impact, -normal, collision.ImpactPos, applyDamage: false); + ApplyImpact(impact, normal, collision.ImpactPos, applyDamage: false); foreach (Submarine dockedSub in submarine.DockedTo) { - dockedSub.SubBody.ApplyImpact(impact, -normal, collision.ImpactPos, applyDamage: false); + dockedSub.SubBody.ApplyImpact(impact, normal, collision.ImpactPos, applyDamage: false); } } @@ -663,7 +677,7 @@ namespace Barotrauma dockedSub.SubBody.ApplyImpact(wallImpact, -impact.Normal, impact.ImpactPos); } - if (cell != null && wallImpact > 0.0f) + if (cell != null && cell.IsDestructible && wallImpact > 0.0f) { var hitWall = Level.Loaded?.ExtraWalls.Find(w => w.Cells.Contains(cell)); if (hitWall != null && hitWall.WallDamageOnTouch > 0.0f) @@ -672,7 +686,7 @@ namespace Barotrauma ConvertUnits.ToDisplayUnits(impact.ImpactPos), 500.0f, hitWall.WallDamageOnTouch, - damageLevelWalls: false); + levelWallDamage: 0.0f); #if CLIENT PlayDamageSounds(damagedStructures, impact.ImpactPos, wallImpact, "StructureSlash"); #endif @@ -800,7 +814,7 @@ namespace Barotrauma #if CLIENT if (Character.Controlled != null && Character.Controlled.Submarine == submarine) { - GameMain.GameScreen.Cam.Shake = impact * 2.0f; + GameMain.GameScreen.Cam.Shake = impact * 10.0f; if (submarine.Info.Type == SubmarineType.Player && !submarine.DockedTo.Any(s => s.Info.Type != SubmarineType.Player)) { float angularVelocity = @@ -814,25 +828,32 @@ namespace Barotrauma foreach (Character c in Character.CharacterList) { if (c.Submarine != submarine) { continue; } - + if (c.KnockbackCooldownTimer > 0.0f) { continue; } + + c.KnockbackCooldownTimer = Character.KnockbackCooldown; + foreach (Limb limb in c.AnimController.Limbs) { if (limb.IsSevered) { continue; } limb.body.ApplyLinearImpulse(limb.Mass * impulse, 10.0f); } - c.AnimController.Collider.ApplyLinearImpulse(c.AnimController.Collider.Mass * impulse, 10.0f); bool holdingOntoSomething = false; if (c.SelectedConstruction != null) { - var controller = c.SelectedConstruction.GetComponent(); - holdingOntoSomething = controller != null && controller.LimbPositions.Any(); + holdingOntoSomething = + c.SelectedConstruction.GetComponent() != null || + (c.SelectedConstruction.GetComponent()?.LimbPositions.Any() ?? false); } - //stun for up to 1 second if the impact equal or higher to the maximum impact - if (impact >= MaxCollisionImpact && !holdingOntoSomething) + if (!holdingOntoSomething) { - c.SetStun(Math.Min(impulse.Length() * 0.2f, 1.0f)); + c.AnimController.Collider.ApplyLinearImpulse(c.AnimController.Collider.Mass * impulse, 10.0f); + //stun for up to 2 second if the impact equal or higher to the maximum impact + if (impact >= MaxCollisionImpact) + { + c.AddDamage(impactPos, AfflictionPrefab.ImpactDamage.Instantiate(3.0f).ToEnumerable(), stun: Math.Min(impulse.Length() * 0.2f, 2.0f), playSound: true); + } } } @@ -843,11 +864,12 @@ namespace Barotrauma item.body.ApplyLinearImpulse(item.body.Mass * impulse, 10.0f); } - + + float dmg = applyDamage ? impact * ImpactDamageMultiplier : 0.0f; var damagedStructures = Explosion.RangedStructureDamage( ConvertUnits.ToDisplayUnits(impactPos), - impact * 50.0f, - applyDamage ? impact * ImpactDamageMultiplier : 0.0f); + impact * 50.0f, + dmg, dmg); #if CLIENT PlayDamageSounds(damagedStructures, impactPos, impact, "StructureBlunt"); 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 8b76495b3..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; @@ -44,6 +44,8 @@ namespace Barotrauma public Hull CurrentHull { get; private set; } + public Level.Tunnel Tunnel; + public SpawnType SpawnType { get { return spawnType; } @@ -97,6 +99,13 @@ namespace Barotrauma { SpawnType = SpawnType.Path; } + +#if CLIENT + if (SubEditorScreen.IsSubEditor()) + { + SubEditorScreen.StoreCommand(new AddOrDeleteCommand(new List { this }, false)); + } +#endif } @@ -177,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) @@ -236,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; @@ -258,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(); @@ -296,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]); } @@ -312,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); @@ -339,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); } @@ -372,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); + } + } } } @@ -460,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; } @@ -471,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 f254b4a65..9242f92cc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs @@ -26,6 +26,9 @@ namespace Barotrauma public readonly Submarine Submarine; public readonly float Condition; + public bool SpawnIfInventoryFull = true; + public bool IgnoreLimbSlots = false; + private readonly Action onSpawned; public ItemSpawnInfo(ItemPrefab prefab, Vector2 worldPosition, Action onSpawned, float? condition = null) @@ -62,9 +65,27 @@ namespace Barotrauma Item spawnedItem; if (Inventory?.Owner != null) { - spawnedItem = new Item(Prefab, Vector2.Zero, null); + if (!SpawnIfInventoryFull && !Inventory.CanBePut(Prefab)) + { + return 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); } } @@ -238,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) + 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) @@ -248,7 +269,11 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue3:ItemPrefabNull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); return; } - spawnQueue.Enqueue(new ItemSpawnInfo(itemPrefab, inventory, onSpawned, condition)); + spawnQueue.Enqueue(new ItemSpawnInfo(itemPrefab, inventory, onSpawned, condition) + { + SpawnIfInventoryFull = spawnIfInventoryFull, + IgnoreLimbSlots = ignoreLimbSlots + }); } public void AddToSpawnQueue(string speciesName, Vector2 worldPosition, Action onSpawn = null) @@ -302,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) { @@ -329,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/NetConfig.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetConfig.cs index 5fca74a45..ddf794d29 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetConfig.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetConfig.cs @@ -14,15 +14,6 @@ namespace Barotrauma.Networking public static string MasterServerUrl = GameMain.Config.MasterServerUrl; - //if a Character is further than this from the sub and the players, the server will disable it - //(in display units) - public const float DisableCharacterDist = 22000.0f; - public const float DisableCharacterDistSqr = DisableCharacterDist * DisableCharacterDist; - - //the character needs to get this close to be re-enabled - public const float EnableCharacterDist = 20000.0f; - public const float EnableCharacterDistSqr = EnableCharacterDist * EnableCharacterDist; - public const float MaxPhysicsBodyVelocity = 64.0f; public const float MaxPhysicsBodyAngularVelocity = 16.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEvent.cs index 09506cf41..33ef8bec8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEvent.cs @@ -18,7 +18,8 @@ namespace Barotrauma.Networking Combine, ExecuteAttack, Upgrade, - AssignCampaignInteraction + AssignCampaignInteraction, + ObjectiveManagerOrderState, } public readonly Entity Entity; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEventManager.cs index ee39ccd9d..4a5441b57 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEventManager.cs @@ -43,30 +43,16 @@ namespace Barotrauma.Networking eventCount++; continue; } - - //the length of the data is written as a byte, so the data needs to be less than 255 bytes long - if (tempEventBuffer.LengthBytes > 255) - { - DebugConsole.ThrowError("Too much data in network event for entity \"" + e.Entity.ToString() + "\" (" + tempEventBuffer.LengthBytes + " bytes, event ID " + e.ID + $", {string.Join(' ',e.Data.Select(d => d.ToString()))})"); - GameAnalyticsManager.AddErrorEventOnce("NetEntityEventManager.Write:TooLong" + e.Entity.ToString(), - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - "Too much data in network event for entity \"" + e.Entity.ToString() + "\" (" + tempEventBuffer.LengthBytes + " bytes, event ID " + e.ID + ")"); - //write an empty event to prevent breaking the event syncing - tempBuffer.Write(Entity.NullEntityID); - tempBuffer.WritePadBits(); - eventCount++; - 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; } tempBuffer.Write(e.EntityID); - tempBuffer.Write((byte)tempEventBuffer.LengthBytes); + tempBuffer.WriteVariableUInt32((uint)tempEventBuffer.LengthBytes); tempBuffer.Write(tempEventBuffer.Buffer, 0, tempEventBuffer.LengthBytes); tempBuffer.WritePadBits(); sentEvents.Add(e); 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/OrderChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs index acee8540e..47233788a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs @@ -1,4 +1,6 @@ -namespace Barotrauma.Networking +using System; + +namespace Barotrauma.Networking { partial class OrderChatMessage : ChatMessage { @@ -13,26 +15,87 @@ //additional instructions (power up, fire at will, etc) public readonly string OrderOption; + public readonly int OrderPriority; + /// /// Used when the order targets a wall /// public int? WallSectionIndex { get; set; } - public OrderChatMessage(Order order, string orderOption, ISpatialEntity targetEntity, Character targetCharacter, Character sender) - : this(order, orderOption, + public OrderChatMessage(Order order, string orderOption, int priority, ISpatialEntity targetEntity, Character targetCharacter, Character sender) + : this(order, orderOption, priority, order?.GetChatMessage(targetCharacter?.Name, sender?.CurrentHull?.DisplayName, givingOrderToSelf: targetCharacter == sender, orderOption: orderOption), targetEntity, targetCharacter, sender) { } - public OrderChatMessage(Order order, string orderOption, string text, ISpatialEntity targetEntity, Character targetCharacter, Character sender) + public OrderChatMessage(Order order, string orderOption, int priority, string text, ISpatialEntity targetEntity, Character targetCharacter, Character sender) : base(sender?.Name, text, ChatMessageType.Order, sender, GameMain.NetworkMember.ConnectedClients.Find(c => c.Character == sender)) { Order = order; OrderOption = orderOption; + OrderPriority = priority; TargetCharacter = targetCharacter; TargetEntity = targetEntity; } + + private void WriteOrder(IWriteMessage msg) + { + msg.Write((byte)Order.PrefabList.IndexOf(Order.Prefab)); + msg.Write(TargetCharacter == null ? (UInt16)0 : TargetCharacter.ID); + msg.Write(TargetEntity is Entity ? (TargetEntity as Entity).ID : (UInt16)0); + + // The option of a Dismiss order is written differently so we know what order we target + // now that the game supports multiple current orders simultaneously + if (Order.Prefab.Identifier != "dismissed") + { + msg.Write((byte)Array.IndexOf(Order.Prefab.Options, OrderOption)); + } + else + { + if (!string.IsNullOrEmpty(OrderOption)) + { + msg.Write(true); + string[] dismissedOrder = OrderOption.Split('.'); + msg.Write((byte)dismissedOrder.Length); + if (dismissedOrder.Length > 0) + { + string dismissedOrderIdentifier = dismissedOrder[0]; + var orderPrefab = Order.GetPrefab(dismissedOrderIdentifier); + msg.Write((byte)Order.PrefabList.IndexOf(orderPrefab)); + if (dismissedOrder.Length > 1) + { + string dismissedOrderOption = dismissedOrder[1]; + msg.Write((byte)Array.IndexOf(orderPrefab.Options, dismissedOrderOption)); + } + } + } + else + { + // If the order option is not specified for a Dismiss order, + // we dismiss all current orders for the character + msg.Write(false); + } + } + + msg.Write((byte)OrderPriority); + msg.Write((byte)Order.TargetType); + if (Order.TargetType == Order.OrderTargetType.Position && TargetEntity is OrderTarget orderTarget) + { + msg.Write(true); + msg.Write(orderTarget.Position.X); + msg.Write(orderTarget.Position.Y); + msg.Write(orderTarget.Hull == null ? (UInt16)0 : orderTarget.Hull.ID); + } + else + { + msg.Write(false); + if (Order.TargetType == Order.OrderTargetType.WallSection) + { + msg.Write((byte)(WallSectionIndex ?? Order.WallSectionIndex ?? 0)); + } + } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs index ddc1107a2..a84520c56 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs @@ -136,8 +136,7 @@ namespace Barotrauma.Networking EnsureBufferSize(ref buf, bitPos + 64); byte[] bytes = BitConverter.GetBytes(val); - WriteBytes(ref buf, ref bitPos, bytes, 0, bytes.Length); - bitPos += 64; + WriteBytes(ref buf, ref bitPos, bytes, 0, 8); } internal static void Write(ref byte[] buf, ref int bitPos, string val) { @@ -155,14 +154,14 @@ namespace Barotrauma.Networking internal static int WriteVariableUInt32(ref byte[] buf, ref int bitPos, uint value) { int retval = 1; - uint num1 = (uint)value; - while (num1 >= 0x80) + uint remainingValue = (uint)value; + while (remainingValue >= 0x80) { - Write(ref buf, ref bitPos, (byte)(num1 | 0x80)); - num1 = num1 >> 7; + Write(ref buf, ref bitPos, (byte)(remainingValue | 0x80)); + remainingValue = remainingValue >> 7; retval++; } - Write(ref buf, ref bitPos, (byte)num1); + Write(ref buf, ref bitPos, (byte)remainingValue); return retval; } @@ -304,19 +303,19 @@ namespace Barotrauma.Networking { int bitLength = buf.Length * 8; - int num1 = 0; - int num2 = 0; + int result = 0; + int shift = 0; while (bitLength - bitPos >= 8) { - byte num3 = ReadByte(buf, ref bitPos); - num1 |= (num3 & 0x7f) << num2; - num2 += 7; - if ((num3 & 0x80) == 0) - return (uint)num1; + byte chunk = ReadByte(buf, ref bitPos); + result |= (chunk & 0x7f) << shift; + shift += 7; + if ((chunk & 0x80) == 0) + return (uint)result; } // ouch; failed to find enough bytes; malformed variable length number? - return (uint)num1; + return (uint)result; } internal static String ReadString(byte[] buf, ref int bitPos) @@ -329,7 +328,7 @@ namespace Barotrauma.Networking if ((ulong)(bitLength - bitPos) < ((ulong)byteLen * 8)) { // not enough data - return null; + return null; } if ((bitPos & 7) == 0) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/LidgrenConnection.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/LidgrenConnection.cs index 94c63e5a4..f255de255 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/LidgrenConnection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/LidgrenConnection.cs @@ -34,6 +34,13 @@ namespace Barotrauma.Networking EndPointString = IPString; } + public override bool SetSteamIDIfUnknown(UInt64 id) + { + if (SteamID != 0) { return false; } //do not allow the SteamID to be set multiple times + SteamID = id; + return true; + } + public override bool EndpointMatches(string endPoint) { if (IPEndPoint?.Address == null) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/NetworkConnection.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/NetworkConnection.cs index 499d99223..4ddf29479 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/NetworkConnection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkConnection/NetworkConnection.cs @@ -35,5 +35,14 @@ namespace Barotrauma.Networking public abstract bool EndpointMatches(string endPoint); public NetworkConnectionStatus Status = NetworkConnectionStatus.Disconnected; + + public virtual bool SetSteamIDIfUnknown(UInt64 id) + { + //by default, don't allow setting the ID, this is only done + //with Lidgren connections since those are initialized before + //the SteamID can be known; it's set once the Steam auth ticket + //is received by the server. + return 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/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index a9d262fa9..8196a5495 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -787,7 +787,7 @@ namespace Barotrauma.Networking private set; } - [Serialize(120.0f, true)] + [Serialize(300.0f, true)] public float KillDisconnectedTime { get; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Voip/VoipConfig.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Voip/VoipConfig.cs index 0c76824fe..ab92eb553 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Voip/VoipConfig.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Voip/VoipConfig.cs @@ -6,8 +6,8 @@ namespace Barotrauma.Networking { static partial class VoipConfig { - public const int MAX_COMPRESSED_SIZE = 120; //amount of bytes we expect each 60ms of audio to fit in + public const int MAX_COMPRESSED_SIZE = 40; //amount of bytes we expect each 20ms of audio to fit in - public static readonly TimeSpan SEND_INTERVAL = new TimeSpan(0,0,0,0,120); + public static readonly TimeSpan SEND_INTERVAL = new TimeSpan(0,0,0,0,20); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs index 6e51f1c45..aeb5d17ed 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs @@ -713,9 +713,10 @@ namespace Barotrauma public void SetPrevTransform(Vector2 simPosition, float rotation) { - if (!IsValidValue(simPosition, "position", -1e10f, 1e10f)) return; - if (!IsValidValue(rotation, "rotation")) return; - +#if DEBUG || UNSTABLE + if (!IsValidValue(simPosition, "position", -1e10f, 1e10f)) { return; } + if (!IsValidValue(rotation, "rotation")) { return; } +#endif prevPosition = simPosition; prevRotation = rotation; } @@ -748,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/Prefabs/IPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IPrefab.cs index fb3e5f630..37cf1891d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/IPrefab.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Security.Cryptography; using System.Text; namespace Barotrauma @@ -11,4 +12,28 @@ namespace Barotrauma string FilePath { get; } ContentPackage ContentPackage { get; } } + + public interface IHasUintIdentifier + { + uint UIntIdentifier { get; set; } + } + + public static class PrefabExtensions + { + public static void CalculatePrefabUIntIdentifier(this T prefab, PrefabCollection prefabs) where T : class, IPrefab, IHasUintIdentifier, IDisposable + { + using (MD5 md5 = MD5.Create()) + { + prefab.UIntIdentifier = ToolBox.StringToUInt32Hash(prefab.Identifier, md5); + + //it's theoretically possible for two different values to generate the same hash, but the probability is astronomically small + var collision = prefabs.Find(p => p != prefab && p.UIntIdentifier == prefab.UIntIdentifier); + if (collision != null) + { + DebugConsole.ThrowError($"Hashing collision when generating uint identifiers for {nameof(T)}: {prefab.Identifier} has the same identifier as {collision.Identifier} ({prefab.UIntIdentifier})"); + collision.UIntIdentifier++; + } + } + } + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ProcGen/Voronoi.cs b/Barotrauma/BarotraumaShared/SharedSource/ProcGen/Voronoi.cs index 46b5e5c10..4442823aa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ProcGen/Voronoi.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ProcGen/Voronoi.cs @@ -987,5 +987,28 @@ namespace Voronoi2 return generateVoronoi(xVal, yVal, 0, width, 0, height); } + public List MakeVoronoiGraph(double[] xVal, double[] yVal, Rectangle area) + { + for (int i = 0; i sites = new HashSet(); + foreach (var graphEdge in graphEdges) + { + graphEdge.Point1 += area.Location.ToVector2(); + graphEdge.Point2 += area.Location.ToVector2(); + sites.Add(graphEdge.Site1); + sites.Add(graphEdge.Site2); + } + foreach (Site site in sites) + { + site.Coord = new DoubleVector2(site.Coord.X + area.Location.X, site.Coord.Y + area.Location.Y); + } + return graphEdges; + } + } // Voronoi Class End } // namespace Voronoi2 End \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs b/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs index 6c6d6e988..feeb54020 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs @@ -153,6 +153,9 @@ namespace Voronoi2 public bool Island; + public bool IsDestructible; + public bool DoesDamage; + public Vector2 Center { get { return new Vector2((float)Site.Coord.X, (float)Site.Coord.Y) + Translation; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs index 1d5dbda99..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); @@ -225,7 +222,10 @@ namespace Barotrauma foreach (PhysicsBody body in PhysicsBody.List) { - if (body.Enabled) { body.SetPrevTransform(body.SimPosition, body.Rotation); } + if (body.Enabled && body.BodyType != FarseerPhysics.BodyType.Static) + { + body.SetPrevTransform(body.SimPosition, body.Rotation); + } } #if CLIENT diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/Screen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/Screen.cs index 3cfa66057..bac118d97 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/Screen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/Screen.cs @@ -24,6 +24,7 @@ { selected.Deselect(); #if CLIENT + GUIContextMenu.CurrentContextMenu = null; GUI.ClearCursorWait(); //make sure any textbox in the previously selected screen doesn't stay selected if (GUI.KeyboardDispatcher.Subscriber != DebugConsole.TextBox) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index feacc748a..0a21bcdce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs @@ -242,7 +242,7 @@ namespace Barotrauma try { - val = Int32.Parse(element.Attribute(name).Value); + val = Int32.Parse(element.Attribute(name).Value, CultureInfo.InvariantCulture); } catch (Exception e) { @@ -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 b2308c19a..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) @@ -87,16 +87,12 @@ namespace Barotrauma { if (this.type != type || !HasRequiredItems(entity)) { return; } if (!Stackable && DelayList.Any(d => d.Parent == this && d.Targets.SequenceEqual(targets))) { return; } - if (delayType == DelayTypes.ReachCursor && Character.Controlled == null) return; + if (delayType == DelayTypes.ReachCursor && Character.Controlled == null) { return; } currentTargets.Clear(); foreach (ISerializableEntity target in targets) { - if (targetIdentifiers != null) - { - //ignore invalid targets - if (!IsValidTarget(target)) { continue; } - } + if (!IsValidTarget(target)) { continue; } currentTargets.Add(target); } @@ -148,7 +144,7 @@ namespace Barotrauma if (element.Delay > 0.0f) { continue; } break; case DelayTypes.ReachCursor: - if (Vector2.Distance(element.Entity.WorldPosition, element.StartPosition.Value) < element.Delay) continue; + if (Vector2.Distance(element.Entity.WorldPosition, element.StartPosition.Value) < element.Delay) { continue; } break; } 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 91e2085c9..091e62f5c 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; @@ -52,7 +53,8 @@ namespace Barotrauma UseTarget = 64, Hull = 128, Limb = 256, - AllLimbs = 512 + AllLimbs = 512, + LastLimb = 1024 } class ItemSpawnInfo @@ -197,11 +199,15 @@ namespace Barotrauma public readonly ActionType type = ActionType.OnActive; - private readonly List explosions; + public readonly List Explosions; private readonly List spawnItems; private readonly List spawnCharacters; + private readonly List triggeredEvents; + private readonly string triggeredEventTargetTag = "statuseffecttarget", + triggeredEventEntityTag = "statuseffectentity"; + private Character user; public readonly float FireSize; @@ -212,6 +218,9 @@ namespace Barotrauma public PhysicsBody sourceBody; + public readonly bool OnlyInside; + public readonly bool OnlyOutside; + public HashSet TargetIdentifiers { get { return targetIdentifiers; } @@ -228,7 +237,9 @@ namespace Barotrauma get { return spawnCharacters; } } - private readonly List> reduceAffliction; + public readonly List> ReduceAffliction; + + public float Duration => duration; //only applicable if targeting NearbyCharacters or NearbyItems public float Range @@ -272,9 +283,12 @@ namespace Barotrauma spawnItems = new List(); spawnCharacters = new List(); Afflictions = new List(); - explosions = new List(); - reduceAffliction = new List>(); + Explosions = new List(); + triggeredEvents = new List(); + ReduceAffliction = new List>(); tags = new HashSet(element.GetAttributeString("tags", "").Split(',')); + OnlyInside = element.GetAttributeBool("onlyinside", false); + OnlyOutside = element.GetAttributeBool("onlyoutside", false); Range = element.GetAttributeFloat("range", 0.0f); Offset = element.GetAttributeVector2("offset", Vector2.Zero); @@ -284,7 +298,7 @@ namespace Barotrauma List targetLimbs = new List(); foreach (string targetLimbName in targetLimbNames) { - if (Enum.TryParse(targetLimbName, out LimbType targetLimb)) { targetLimbs.Add(targetLimb); } + if (Enum.TryParse(targetLimbName, ignoreCase: true, out LimbType targetLimb)) { targetLimbs.Add(targetLimb); } } if (targetLimbs.Count > 0) { this.targetLimbs = targetLimbs.ToArray(); } } @@ -310,7 +324,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)) @@ -352,6 +366,12 @@ namespace Barotrauma lifeTime = attribute.GetAttributeFloat(0); lifeTimer = lifeTime; break; + case "eventtargettag": + triggeredEventTargetTag = attribute.Value; + break; + case "evententitytag": + triggeredEventEntityTag = attribute.Value; + break; case "checkconditionalalways": CheckConditionalAlways = attribute.GetAttributeBool(false); break; @@ -402,7 +422,7 @@ namespace Barotrauma switch (subElement.Name.ToString().ToLowerInvariant()) { case "explosion": - explosions.Add(new Explosion(subElement, parentDebugName)); + Explosions.Add(new Explosion(subElement, parentDebugName)); break; case "fire": FireSize = subElement.GetAttributeFloat("size", 10.0f); @@ -477,7 +497,7 @@ namespace Barotrauma if (subElement.Attribute("name") != null) { DebugConsole.ThrowError("Error in StatusEffect (" + parentDebugName + ") - define afflictions using identifiers or types instead of names."); - reduceAffliction.Add(new Pair( + ReduceAffliction.Add(new Pair( subElement.GetAttributeString("name", "").ToLowerInvariant(), subElement.GetAttributeFloat(1.0f, "amount", "strength", "reduceamount"))); } @@ -488,7 +508,7 @@ namespace Barotrauma if (AfflictionPrefab.List.Any(ap => ap.Identifier == name || ap.AfflictionType == name)) { - reduceAffliction.Add(new Pair( + ReduceAffliction.Add(new Pair( name, subElement.GetAttributeFloat(1.0f, "amount", "strength", "reduceamount"))); } @@ -502,6 +522,23 @@ namespace Barotrauma var newSpawnItem = new ItemSpawnInfo(subElement, parentDebugName); if (newSpawnItem.ItemPrefab != null) { spawnItems.Add(newSpawnItem); } break; + case "triggerevent": + string identifier = subElement.GetAttributeString("identifier", null); + if (!string.IsNullOrWhiteSpace(identifier)) + { + EventPrefab prefab = EventSet.GetEventPrefab(identifier); + if (prefab != null) + { + 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); if (!string.IsNullOrWhiteSpace(newSpawnCharacter.SpeciesName)) { spawnCharacters.Add(newSpawnCharacter); } @@ -555,8 +592,7 @@ namespace Barotrauma { foreach (Character c in Character.CharacterList) { - if (!c.Enabled || c.Removed || !IsValidTarget(c)) { continue; } - if (CheckDistance(c)) + if (c.Enabled && !c.Removed && CheckDistance(c) && IsValidTarget(c)) { targets.Add(c); } @@ -564,12 +600,27 @@ namespace Barotrauma } if (HasTargetType(TargetType.NearbyItems)) { - foreach (Item item in Item.ItemList) + //optimization for powered components that can be easily fetched from Powered.PoweredList + if (targetIdentifiers.Count == 1 && + (targetIdentifiers.Contains("powered") || targetIdentifiers.Contains("junctionbox") || targetIdentifiers.Contains("relaycomponent"))) { - if (item.Removed || !IsValidTarget(item)) { continue; } - if (CheckDistance(item)) + foreach (Powered powered in Powered.PoweredList) { - targets.AddRange(item.AllPropertyObjects); + Item item = powered.Item; + if (!item.Removed && CheckDistance(item) && IsValidTarget(item)) + { + targets.AddRange(item.AllPropertyObjects); + } + } + } + else + { + foreach (Item item in Item.ItemList) + { + if (!item.Removed && CheckDistance(item) && IsValidTarget(item)) + { + targets.AddRange(item.AllPropertyObjects); + } } } } @@ -590,25 +641,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 { @@ -627,15 +683,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 { @@ -648,8 +709,8 @@ namespace Barotrauma continue; } } - if (!pc.Matches(target)) { return false; } } + if (targets.None(t => pc.Matches(t))) { return false; } } } return true; @@ -660,34 +721,57 @@ namespace Barotrauma protected bool IsValidTarget(ISerializableEntity entity) { - if (targetIdentifiers == null) { return true; } - if (entity is Item item) { - if (targetIdentifiers.Contains("item")) { return true; } - if (item.HasTag(targetIdentifiers)) { return true; } - if (targetIdentifiers.Any(id => id.Equals(item.Prefab.Identifier, StringComparison.OrdinalIgnoreCase))) { return true; } + return IsValidTarget(item); } else if (entity is ItemComponent itemComponent) { - if (targetIdentifiers.Contains("itemcomponent")) { return true; } - if (itemComponent.Item.HasTag(targetIdentifiers)) { return true; } - if (targetIdentifiers.Any(id => id.Equals(itemComponent.Item.Prefab.Identifier, StringComparison.OrdinalIgnoreCase))) { return true; } + return IsValidTarget(itemComponent); } 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; } } else if (entity is Character character) { - if (targetIdentifiers.Contains("character")) { return true; } - if (targetIdentifiers.Any(id => id.Equals(character.SpeciesName, StringComparison.OrdinalIgnoreCase))) { return true; } + return IsValidTarget(character); } - + if (targetIdentifiers == null) { return true; } return targetIdentifiers.Any(id => id.Equals(entity.Name, StringComparison.OrdinalIgnoreCase)); } + protected bool IsValidTarget(ItemComponent itemComponent) + { + 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)); + } + + protected bool IsValidTarget(Item item) + { + if (OnlyInside && item.CurrentHull == null) { return false; } + if (OnlyOutside && item.CurrentHull != null) { return false; } + if (targetIdentifiers == null) { return true; } + if (targetIdentifiers.Contains("item")) { return true; } + if (item.HasTag(targetIdentifiers)) { return true; } + return targetIdentifiers.Any(id => id.Equals(item.Prefab.Identifier, StringComparison.OrdinalIgnoreCase)); + } + + protected bool IsValidTarget(Character character) + { + if (OnlyInside && character.CurrentHull == null) { return false; } + if (OnlyOutside && character.CurrentHull != null) { return false; } + if (targetIdentifiers == null) { return true; } + if (targetIdentifiers.Contains("character")) { return true; } + return targetIdentifiers.Any(id => id.Equals(character.SpeciesName, StringComparison.OrdinalIgnoreCase)); + } + public void SetUser(Character user) { this.user = user; @@ -701,7 +785,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) { @@ -726,11 +810,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); } @@ -781,9 +861,9 @@ namespace Barotrauma Vector2 position = worldPosition ?? (entity == null || entity.Removed ? Vector2.Zero : entity.WorldPosition); if (worldPosition == null) { - if (entity is Character c && targetLimbs?.FirstOrDefault(l => l != LimbType.None) is LimbType l) + if (entity is Character character && !character.Removed && targetLimbs?.FirstOrDefault(l => l != LimbType.None) is LimbType limbType) { - Limb limb = c.AnimController.GetLimb(l); + Limb limb = character.AnimController.GetLimb(limbType); if (limb != null && !limb.Removed) { position = limb.WorldPosition; @@ -791,8 +871,7 @@ namespace Barotrauma } else { - var targetLimb = targets.FirstOrDefault(t => t is Limb) as Limb; - if (targetLimb != null && !targetLimb.Removed) + if (targets.FirstOrDefault(t => t is Limb) is Limb targetLimb && !targetLimb.Removed) { position = targetLimb.WorldPosition; } @@ -884,9 +963,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; } @@ -901,13 +980,15 @@ namespace Barotrauma } } - foreach (Explosion explosion in explosions) + foreach (Explosion explosion in Explosions) { explosion.Explode(position, damageSource: entity, attacker: user); } 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) { @@ -942,7 +1023,7 @@ namespace Barotrauma } } - foreach (Pair reduceAffliction in reduceAffliction) + foreach (Pair reduceAffliction in ReduceAffliction) { float reduceAmount = disableDeltaTime ? reduceAffliction.Second : reduceAffliction.Second * deltaTime; Limb targetLimb = null; @@ -981,6 +1062,34 @@ namespace Barotrauma } bool isNotClient = GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient; + if (isNotClient && GameMain.GameSession?.EventManager is { } eventManager) + { + foreach (EventPrefab eventPrefab in triggeredEvents) + { + Event ev = eventPrefab.CreateInstance(); + if (ev == null) { continue; } + eventManager.QueuedEvents.Enqueue(ev); + + if (ev is ScriptedEvent scriptedEvent) + { + if (!string.IsNullOrWhiteSpace(triggeredEventTargetTag)) + { + 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 }); + } + } + } + } + if (isNotClient && entity != null && Entity.Spawner != null) //clients are not allowed to spawn entities { foreach (CharacterSpawnInfo characterSpawnInfo in spawnCharacters) @@ -1042,11 +1151,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 { @@ -1057,25 +1166,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; @@ -1092,12 +1194,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; } } @@ -1204,7 +1307,7 @@ namespace Barotrauma } } - foreach (Pair reduceAffliction in element.Parent.reduceAffliction) + foreach (Pair reduceAffliction in element.Parent.ReduceAffliction) { Limb targetLimb = null; Character targetCharacter = null; diff --git a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs index 61f6dc39f..6c654be44 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 @@ -333,24 +333,24 @@ namespace Barotrauma if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } - if (gameSession.Mission != null) + foreach (Mission mission in gameSession.Missions) { - if (gameSession.Mission is CombatMission combatMission && GameMain.GameSession.WinningTeam.HasValue) + if (mission is CombatMission combatMission && GameMain.GameSession.WinningTeam.HasValue) { //all characters that are alive and in the winning team get an achievement - UnlockAchievement(gameSession.Mission.Prefab.AchievementIdentifier + (int)GameMain.GameSession.WinningTeam, true, + UnlockAchievement(mission.Prefab.AchievementIdentifier + (int)GameMain.GameSession.WinningTeam, true, c => c != null && !c.IsDead && !c.IsUnconscious && combatMission.IsInWinningTeam(c)); } - else if (gameSession.Mission.Completed) + else if (mission.Completed) { //all characters get an achievement if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { - UnlockAchievement(gameSession.Mission.Prefab.AchievementIdentifier, true, c => c != null); + UnlockAchievement(mission.Prefab.AchievementIdentifier, true, c => c != null); } else { - UnlockAchievement(gameSession.Mission.Prefab.AchievementIdentifier); + UnlockAchievement(mission.Prefab.AchievementIdentifier); } } } @@ -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/Upgrade.cs b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs index 90f9fe3ae..65891b222 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/Upgrade.cs @@ -18,7 +18,7 @@ namespace Barotrauma private readonly string Multiplier; - private readonly char[] prefixCharacters = { '=', '/', '*', 'x', '-', '+' }; + private static readonly char[] prefixCharacters = { '=', '/', '*', 'x', '-', '+' }; private readonly Upgrade upgrade; @@ -66,7 +66,7 @@ namespace Barotrauma } else { - float multiplier = UpgradePrefab.ParsePercentage(Multiplier, Name, sourceElement, upgrade.Prefab.SupressWarnings); + float multiplier = UpgradePrefab.ParsePercentage(Multiplier, Name, sourceElement, upgrade.Prefab.SuppressWarnings); return ApplyPercentage(value, multiplier, level); } } @@ -79,6 +79,46 @@ namespace Barotrauma return 0; } + public static float CalculateUpgrade(object originalValue, int level, string Multiplier) + { + if (originalValue is float || originalValue is int || originalValue is double) + { + var value = (float)originalValue; + + if (Multiplier[^1] != '%') + { + float multiplier = 1.0f; + if (Multiplier.Length > 1) + { + if (prefixCharacters.Contains(Multiplier[0])) + { + float.TryParse(Multiplier.Substring(1).Trim(), NumberStyles.Number, CultureInfo.InvariantCulture, out multiplier); + } + } + switch (Multiplier[0]) + { + case '*': + case 'x': + return value * (multiplier * level); + case '/': + return value / (multiplier * level); + case '-': + return value - (multiplier * level); + case '+': + return value + (multiplier * level); + case '=': + return multiplier; + } + } + else + { + float multiplier = UpgradePrefab.ParsePercentage(Multiplier, "", suppressWarnings: true); + return ApplyPercentage(value, multiplier, level); + } + } + return float.NaN; + } + /// /// Sets the OriginalValue to a value stored in the save XML element /// @@ -125,7 +165,7 @@ namespace Barotrauma } } - if (!upgrade.Prefab.SupressWarnings) + if (!upgrade.Prefab.SuppressWarnings) { DebugConsole.AddWarning($"Multiplier for {Name} is too short or does not contain proper prefix. \n" + $"The value should start with {string.Join(",", prefixCharacters)} and contain a floating point value or another property. \n" + @@ -305,7 +345,7 @@ namespace Barotrauma subElement.Add(new XElement(propertyRef.Name, new XAttribute("value", propertyRef.OriginalValue))); } - else if (!Prefab.SupressWarnings) + else if (!Prefab.SuppressWarnings) { DebugConsole.AddWarning($"Failed to save upgrade \"{Prefab.Name}\" on {TargetEntity.Name} because property reference \"{propertyRef.Name}\" is missing original values. \n" + "Upgrades should always call Upgrade.ApplyUpgrade() or manually set the original value in a property reference after they have been added. \n" + @@ -340,22 +380,6 @@ namespace Barotrauma propertyReference.SetOriginalValue(originalValue); object newValue = Convert.ChangeType(propertyReference.CalculateUpgrade(Level, sourceElement), originalValue.GetType(), NumberFormatInfo.InvariantInfo); property!.SetValue(entity, newValue); -#if SERVER - // if (TargetEntity is IServerSerializable clientSerializable && !IsEqual(originalValue, newValue)) - // { - // GameMain.Server.CreateEntityEvent(clientSerializable, new object[] { NetEntityEvent.Type.ChangeProperty, property }); - // } - // - // static bool IsEqual(object item1, object item2) - // { - // if (item1 is float float1 && item2 is float float2) - // { - // return MathUtils.NearlyEqual(float1, float2); - // } - // - // return item1 == item2; - // } -#endif } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs index 1472aef42..dc0173d3e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs @@ -23,16 +23,16 @@ namespace Barotrauma Prefab = prefab; IncreaseLow = UpgradePrefab.ParsePercentage(element.GetAttributeString("increaselow", string.Empty), - "IncreaseLow", element, suppressWarnings: prefab.SupressWarnings); + "IncreaseLow", element, suppressWarnings: prefab.SuppressWarnings); IncreaseHigh = UpgradePrefab.ParsePercentage(element.GetAttributeString("increasehigh", string.Empty), - "IncreaseHigh", element, suppressWarnings: prefab.SupressWarnings); + "IncreaseHigh", element, suppressWarnings: prefab.SuppressWarnings); BasePrice = element.GetAttributeInt("baseprice", -1); if (BasePrice == -1) { - if (prefab.SupressWarnings) + if (prefab.SuppressWarnings) { DebugConsole.AddWarning($"Price attribute \"baseprice\" is not defined for {prefab?.Identifier}.\n " + "The value has been assumed to be '1000'."); @@ -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)); } @@ -143,7 +147,7 @@ namespace Barotrauma private bool Disposed { get; set; } - public bool SupressWarnings { get; } + public bool SuppressWarnings { get; } public bool HideInMenus { get; } @@ -159,7 +163,7 @@ namespace Barotrauma Description = element.GetAttributeString("description", string.Empty); MaxLevel = element.GetAttributeInt("maxlevel", 1); Identifier = element.GetAttributeString("identifier", ""); - SupressWarnings = element.GetAttributeBool("supresswarnings", false); + SuppressWarnings = element.GetAttributeBool("supresswarnings", false); HideInMenus = element.GetAttributeBool("hideinmenus", false); FilePath = filePath; SourceElement = element; @@ -219,7 +223,7 @@ namespace Barotrauma string[] categories = element.GetAttributeStringArray("categories", new string[] { }); UpgradeCategories = (from category in UpgradeCategory.Categories from identifier in categories where string.Equals(category.Identifier, identifier) select category).ToArray(); - if (!SupressWarnings && !IsOverride) + if (!SuppressWarnings && !IsOverride) { foreach (UpgradePrefab matchingPrefab in Prefabs.Where(prefab => prefab.TargetItems.Any(s => TargetItems.Contains(s)))) { @@ -243,9 +247,14 @@ namespace Barotrauma Prefabs.Add(this, isOverride); } - public static UpgradePrefab? Find(string idenfitier) + public bool IsDisallowed(Item item) { - return !string.IsNullOrWhiteSpace(idenfitier) ? Prefabs.Find(prefab => prefab.Identifier == idenfitier) : null; + return item.disallowedUpgrades.Contains(Identifier); + } + + public static UpgradePrefab? Find(string identifier) + { + return !string.IsNullOrWhiteSpace(identifier) ? Prefabs.Find(prefab => prefab.Identifier == identifier) : null; } public static void LoadAll(IEnumerable files) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/IdRemap.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/IdRemap.cs index b1c8ab169..af7097579 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/IdRemap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/IdRemap.cs @@ -1,4 +1,5 @@ using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; @@ -11,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()) @@ -25,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 { @@ -41,7 +42,7 @@ namespace Barotrauma private void InsertId(int id) { - for (int i=0;i id) { @@ -65,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; } @@ -89,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); } @@ -99,5 +100,16 @@ namespace Barotrauma } return 0; } + + public static ushort DetermineNewOffset() + { + ushort idOffset = 0; + foreach (Entity e in Entity.GetEntities()) + { + if (e.ID > Entity.ReservedIDStart || e is Submarine) { continue; } + idOffset = Math.Max(idOffset, e.ID); + } + return idOffset; + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs index d1cc6da73..d52c654f3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs @@ -678,27 +678,11 @@ namespace Barotrauma public static List TriangulateConvexHull(List vertices, Vector2 center) { List triangles = new List(); - - int triangleCount = vertices.Count - 2; - vertices.Sort(new CompareCCW(center)); - - int lastIndex = 1; - for (int i = 0; i < triangleCount; i++) + for (int i = 0; i < vertices.Count; i++) { - Vector2[] triangleVertices = new Vector2[3]; - triangleVertices[0] = vertices[0]; - int k = 1; - for (int j = lastIndex; j <= lastIndex + 1; j++) - { - triangleVertices[k] = vertices[j]; - k++; - } - lastIndex += 1; - - triangles.Add(triangleVertices); + triangles.Add(new Vector2[3] { center, vertices[i], vertices[(i + 1) % vertices.Count] }); } - return triangles; } @@ -739,7 +723,7 @@ namespace Barotrauma return wrappedPoints; } - public static List GenerateJaggedLine(Vector2 start, Vector2 end, int iterations, float offsetAmount) + public static List GenerateJaggedLine(Vector2 start, Vector2 end, int iterations, float offsetAmount, Rectangle? bounds = null) { List segments = new List { @@ -761,6 +745,26 @@ namespace Barotrauma normal = new Vector2(-normal.Y, normal.X); midPoint += normal * Rand.Range(-offsetAmount, offsetAmount, Rand.RandSync.Server); + if (bounds.HasValue) + { + if (midPoint.X < bounds.Value.X) + { + midPoint.X = bounds.Value.X + (bounds.Value.X - midPoint.X); + } + else if (midPoint.X > bounds.Value.Right) + { + midPoint.X = bounds.Value.Right - (midPoint.X - bounds.Value.Right); + } + if (midPoint.Y < bounds.Value.Y) + { + midPoint.Y = bounds.Value.Y + (bounds.Value.Y - midPoint.Y); + } + else if (midPoint.Y > bounds.Value.Bottom) + { + midPoint.Y = bounds.Value.Bottom - (midPoint.Y - bounds.Value.Bottom); + } + } + segments.Insert(i, new Vector2[] { startSegment, midPoint }); segments.Insert(i + 1, new Vector2[] { midPoint, endSegment }); @@ -916,6 +920,8 @@ namespace Barotrauma return (float)Math.Pow(f, p); } + public static float Pow2(float f) => f * f; + /// /// Converts the alignment to a vector where -1,-1 is the top-left corner, 0,0 the center and 1,1 bottom-right /// @@ -946,12 +952,10 @@ namespace Barotrauma /// Modified from: /// http://www.gamefromscratch.com/post/2012/11/24/GameDev-math-recipes-Rotating-one-point-around-another-point.aspx /// - public static Vector2 RotatePointAroundTarget(Vector2 point, Vector2 target, float degrees, bool clockWise = true) + public static Vector2 RotatePointAroundTarget(Vector2 point, Vector2 target, float radians, bool clockWise = true) { - // (Math.PI / 180) * degrees - var angle = MathHelper.ToRadians(degrees); - var sin = Math.Sin(angle); - var cos = Math.Cos(angle); + var sin = Math.Sin(radians); + var cos = Math.Cos(radians); if (!clockWise) { sin = -sin; 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(), ""); @@ -438,6 +449,7 @@ namespace Barotrauma return key; } + /// /// Returns a new instance of the class with all properties and fields copied. /// @@ -572,7 +584,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 +596,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 065f88283..14074f2dd 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 613bd67bd..b2208771e 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 713442c9e..c12a1e85c 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 5b6e5d0a8..3c5994800 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 e3317c35c..9f783fe83 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 90bcc7fb5..0b90165e1 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..28b1c9410 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 279fb80bc..da40ce2e5 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 2bf89aee5..52b6147ed 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 0d0b0d76a..f384a238b 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..943887519 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..4075081a5 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 9a70b2bb9..b0fb3144b 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 90919f251..aecb572ff 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..160f47b2f 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 c125aa4bd..af4053a15 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,61 +1,386 @@ --------------------------------------------------------------------------------------------------------- -v0.1100.0.4 (unstable) +v0.12.X.X --------------------------------------------------------------------------------------------------------- -- Improvements to spineling. -- More improvements to level art and layouts. -- Added new military outpost music track. -- Added mineral scanning functionality for the sonars of Berilia and R-29. -- Devices in beacon stations no longer deteriorate by themselves. -- Added an airlock on the top of the beacon stations. -- Deep Diver subs can dive 20% deeper than other subs without getting crushed. -- Added mineral mining missions. +Campaign changes: +- Replaced "Lair" locations with "hunting grounds" in the connections between levels. Inhabited locations next to hunting grounds have a chance of getting abandoned and habitation can't spread through those levels to adjacent locations. The hunting grounds can be cleared by killing a boss monster in the level. +- Beacon stations are shown on the campaign map. +- Visual changes to the map to make it a bit more intuitive. +- When you enter a level with a beacon station, you always get an optional side objective to restore it even if you haven't selected a beacon mission. +- Added radiation on the campaign map: the instensity of the radiation around Jupiter is slowly increasing, which is forcing Europans to delve deeper under the ice. In practice, the radiation gradually destroys the outposts starting from the left side of the map, making it more dangerous and costly to stay in these areas. The intention behind this is to prevent players from farming resources indefinitely in the low-difficulty areas of the game before proceeding further. + +Abyss: +- Reintroduced Endworms. +- Added floating islands that contain caves and rare minerals to the Abyss. +- Multiple current orders: you can assign up to three simultaneous orders for characters and drag the icons to change their priority. + +Changes: +- Added abandoned outposts and a new abandoned outpost mission type. +- Added entity subcategories to the submarine editor (note that most of the vanilla items/structures aren't categorized yet in this build). +- Reworked attacks and effects for the following creatures: Moloch, Black Moloch, Hammerhead, Hammerhead Matriarch, and Golden Hammerhead. They now have bigger impact on the sub when they hit it. Black Moloch's emp damage is halved. +- Moloch's shell now always breaks when shot with a railgun. +- Recreated waypoints for the vanilla subs. +- Added EMP effect to nuclear shells. +- Added a right click context menu option to copy debug console errors to clipboard. +- Railgun shells now explode instead of piercing armor. Physicorium shells still go through armor. + +Fixes: +- Major improvements to the voice chat: higher audio quality, less intrusive radio effect, fixed clicks/distortion. +- Fixed clients' class preferences not being respected in all cases where they should, resulting some client getting a class that should be give to another client. +- Items can't be stacked in the equip slots (e.g. you can't hold and throw a stack of grenades). +- Trying to pick up a stack of items with a full inventory doesn't pick up the item in both hand slots. +- Fixed turrets getting cropped by the "wikiimage" console command. +- Fixed items with nothing but a holdable component (e.g. medical items) being hidden when hiding wires in the sub editor. +- Fixed ban list not displaying ban reasons or the duration of the ban. +- Fixed items appearing misplaced when switching to the sub editor mid-round with console commands. +- Fixed shuttles spawning undocked in certain custom subs (e.g. DOMA Prototype Mk 8). +- Fixed "tried to add a dead character to CrewManager" console error when reviving a character who died of high pressure. +- Fixed skill levels not getting updated in the "my character" tab mid-round. +- Fixed clients not calculating eventmanager intensity, preventing any high-intensity tracks from playing in multiplayer. +- Fixed campaign map being closed with RMB even if the user has inverted their mouse buttons. +- Fixed damage sometimes desyncing when the sub hits walls/spires: happened because damage was also applied client-side, which sometimes caused the clients to damage walls that weren't actually hit server-side. +- Fixed physics bodies between docking ports not getting mirrored in mirrored subs, sometimes causing impassable areas in spots where the docking port used to be. +- Fixed status monitor in a docked shuttle sometimes not displaying the main sub. +- Fixed campaign stores sometimes displaying prices for a previous location. +- Fixed various scaling issues on higher resolutions. + +Bots: +- Bots now warn when you are running low / out of oxygen or welding fuel tanks, turret ammunition, or reactor fuel. +- Bots no longer report dead monsters on the sonar. +- Disabled the dialogue lines about not finding any targets for the given order, since they don't really improve the feedback. Instead they can give a wrong impression that the bot is not following the order when they actually are. +- Bots no longer speak about not being able to reach the diving suit when they fail to get it because it was taken by someone else. They will target another suit like they used to. +- Bots don't extinguish fires when there's ballast flora in the hull. +- Bots only speak about getting attacked when attacked by players. Fixes security bots throwing ultimatums on other bots that accidentally damage them (e.g. while welding). +- Bots that operate an item (reactor/turret) now drop the empty items on the floor instead of placing them back to a container. This saves time and the empty items should be taken care of by the idle bots. +- Fixed bots not seeking more oxygen before the remaining oxygen level drops to zero when they already have diving gear equipped and when they are staying stationary, leading to suffocation when the bot is repairing/operating while already wearing the diving gear and running out of oxygen tanks. +- Fixed bots trying to swap the oxygen tank too early when they are outside of the sub, which in some cases might lead to bugs where they try to get back inside the submarine. +- Improved the feedback by adding dialogues for swapping the oxygen and for not being able to find more oxygen. +- Fixed bots trying to return the diving suit when it's not a reasonable thing to do, but when they actually don't need it. +- Fixed bots failing to swap an oxygen tank from a diving mask to a diving suit when both items are equipped. +- Fixed bots getting confused when they have a mask without a valid oxygen tank inside it and when there's not enough oxygen in the room to be without the mask. + +Modding: +- Explosions now have three new parameters: ignorecover, onlyinside, and onlyoutside. +- Fixed crashing when attempting to play a music clip that isn't a valid ogg file or if the file is not found. +- Fixed crashing when trying to spawn a character variant with custom inventory contents. + +--------------------------------------------------------------------------------------------------------- +v0.12.0.3 +--------------------------------------------------------------------------------------------------------- + +- Fixed inability to permanently ban players who are currently in the server. +- Fixed scrap items failing to load in saves done prior to v0.12. +- Fixed bandage/plastiseal crafting exploit. +- Fixed crashing if a bot tries to operate a turret that's linked to some other item than a loader. + +--------------------------------------------------------------------------------------------------------- +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 destroyed walls not disappearing client-side. -- Fixed linked submarines not loading. -- Fixed occasional "missing entity" errors caused by the server failing to write an ID card's data in a network message. -- Fixed 'ID taken by Galldren' error when placing ItemAssemblies in submarine editor. -- Fixed a bug that sometimes caused power to desync in multiplayer: when connecting the second end of a wire to a device other than a junction box, the server would sometimes not register the wire as being connected. -- The player initiating a ready check no longer has to answer the the check. -- Fixed a crash caused by the ready check. -- Fixed hull upgrades not affecting the sub's crush depth. -- Fixed console errors when spineling's spike gets stuck to a door. +- 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.1100.0.3 (unstable) +v0.11.0.10 --------------------------------------------------------------------------------------------------------- -- Added beacon missions where you have to repair and power up a "beacon station". -- Misc level generation improvements. -- Improvements to level art. -- Handheld sonar can be used to scan for minerals. -- More descriptive bot dialog when they can't find items they're looking for. -- Improved the way ballast flora drains power from the sub: instead of a static load, the load fluctuates (and causes more problems with the grid!). -- Made destructible ice walls (including spires) easier to destroy. -- Made ballast flora spores less common and removed them altogether from Cold Caverns. -- Made autopilot better at avoiding ice spires. -- Bots keep more distance to the player while following underwater and outside the sub. -- Reduced scrap spawn rates in wrecks. -- Removed the explosive cargo mission variant where one of the explosives spontaneously explodes. -- Improved the way destructible cells break into smaller fragments (ice spires in particular). -- Split combat missions into a separate game mode. -- Added mineral scanning functionality for sonars: enabled by default for handheld sonars, can be enabled for other sonar devices in the Sub Editor with Sonar component's "HasMineralScanner" property - -Bugfixes: -- Fixed frequent "level mismatch" errors in multiplayer due to mismatching resource spawns. -- Fixed wires not appearing in some item assemblies placed in the sub editor until the sub is saved. -- Fixed decorative sprites being positioned incorrectly on mirrored items (such as pumps and fabricators). -- Fixed sonar somatimes calculating the distance to targets incorrectly. -- Fixed caves sometimes spawning in open water. -- Fixed level floor not being rendered fully when zoomed in. -- Fixed nest's sonar label being positioned incorrectly client-side. +- Fixed monster missions causing a disconnect in multiplayer mission mode. +- Fixed bots getting removed from the crew when starting a multiplayer campaign, returning to the lobby during the first round and then reloading the campaign. +- Fixed crashing when entering a new level in the campaign when an inactive pump has been infected with ballast flora. +- Fixed ballast flora branches respawning instantly if they're destroyed while they're growing towards a target. +- Fixed crashing when attempting to place components outside of the submarine in test mode. +- Fixed inability to rewire beacon stations when rewiring is disabled on the server. +- Fixed repair tools that aren't held causing a crash upon use (only affects modded items). +- Fixed raycast weapons (revolvers, shotguns, SMGs) sometimes not hitting monsters in specific areas outside the sub. +- Fixed submarine's price field being difficult to edit in the sub editor due to the value getting clamped above the minimum price while typing in the box. +- Potential fix to certain projectiles (e.g. harpoons, spineling's spikes) sometimes causing erratic physics behavior and errors (ragdolls going crazy, submarine getting launched off at a high velocity...) when they stick to the submarine or to characters. +- Fixed occasional crashes when swapping to another character's ID card with a mask or diving suit on. --------------------------------------------------------------------------------------------------------- -v0.1100.0.2 (unstable) +v0.11.0.9 --------------------------------------------------------------------------------------------------------- -Overhauled environments (WIP): +- Fixed operating a pump manually causing a disconnect in multiplayer. +- Fixed ruins sometimes spawning partially inside level walls. +- Fixed the game occasionally failing to generate a crash report when it crashes. + +--------------------------------------------------------------------------------------------------------- +v0.11.0.8 +--------------------------------------------------------------------------------------------------------- + +- Fixed monsters being hard to hit with ranged weapons when far from the players in multiplayer (e.g. when operating a drone far away from the sub). +- Fixed occasional console errors when loading a submarine with a ballast flora infection. +- Fixed ambient lighting not affecting the submarine's outer walls. +- Fixed wire nodes getting messed up between campaign rounds if the wire was mirrored horizontally while placing it. +- Fixed ruins sometimes spawning with some of the wires disconnected. +- Fixed ice spires sometimes spawning too close to the start/end of the level. +- Render egg sprites in front of minerals. +- Fixed dancing coilguns. + +--------------------------------------------------------------------------------------------------------- +v0.11.0.7 +--------------------------------------------------------------------------------------------------------- + +Environment overhaul: - Remade textures. - Branching level paths. - More varied level layouts. @@ -66,54 +391,22 @@ Overhauled environments (WIP): - Piezo crystals: environmental objects that drain power from the submarine when you get too close to them. - The biomes further on the campaign map are deeper down in the ocean, meaning that crush depth starts higher up in the level. The biomes near the end of the map require hull upgrades to traverse safely. - Improved resource (minerals and plants) spawning: resources now spawn in clusters which can contain multiple instances of the same resource. - -Additions and changes: -- Added nest missions where you need to enter a cave to destroy a monster nest. -- Submarine hull upgrades increase the submarine's tolerance to pressure, allowing it to dive deeper without getting crushed by pressure. -- Added damage particles when dealing damage to ballast flora. -- Added lights to spineling's spikes. - Made thermal artifacts a bit more manageable: they now start a fires periodically, not continuously. -- Made ballast flora more vulnerable to fire. -- Damaging ballast flora increases karma. -Bugfixes: -- Fixed projectiles sometimes phasing through the spineling without damaging it. -- Fixed spineling's spike projectiles behaving erratically in multiplayer. -- Fixed in ability to damage huskified crew members if friendly fire is disabled. -- Fixed prototype steam cannon particles going through walls. -- Fixed "engineers are special" outpost event not giving XP when successfully helping the NPC. -- Fixed "propaganda" and "clown outbreak" outpost events not triggering. - ---------------------------------------------------------------------------------------------------------- -v0.1100.0.1 (unstable) ---------------------------------------------------------------------------------------------------------- - -- Added a new monster, "Spineling". -- Fixed oxygen generators. -- Added concatenation component (a signal component that joins two inputs together). -- Added toxin attack to ballast flora. -- Fixed flamer doing more damage to the ballast flora than it should. -- Fixed projectiles being blocked by ballast flora. -- Fixed ballast flora sometimes closing doors permanently. -- Made welding tools, plasma cutters and explosives hurt ballast flora. -- Fixed console errors when setting an engine's max force to 0. -- Fixed crashing when a disguised human turns into a husk. -- Fixed disconnected, hanging wires sometimes appearing at the wrong end of the wire. -- Fixed progress bar saying "welding" instead of "cutting" when cutting open a welded door. -- Fixed explosion damage to items not being diminished if there are obstacles between the explosion and the item. -- Fixes to flamer particles going through walls. -- Fixed items that are included in multiple categories (e.g. oxygen tanks, battery cells) not appearing in the sub editor's entity list unless using the search bar. -- Fixed psychosis sounds affecting all players. -- Fixed fabricators and deconstructors deteriorating even if they're not running. -- Fixed crashing when selecting a fabricator linked to a deconstructor or vice versa. -- Fixed "hide offensive server names" tickbox working the wrong way around in the server browser. - ---------------------------------------------------------------------------------------------------------- -v0.1100.0.0 (unstable) ---------------------------------------------------------------------------------------------------------- +New missions: +- Nest missions where you need to enter a cave to destroy a monster nest. +- Beacon missions where you have to repair and power up a "beacon station". +- Mineral collection missions where you have to locate and mine a mineral cluster. Additions and changes: - Added ballast flora, a plant-like organism that can infect the submarine and leech power from junction boxes and batteries. A ballast flora infection may be contracted by passing through a colony of ballast flora spores (which are faintly visible on the sonar). +- Added a new monster, "Spineling". +- Adjustments and balancing to the way locations change to other types of locations: habitation now spreads faster as the player explores the campaign map further. +- Added new military outpost music track. +- Submarine hull upgrades increase the submarine's tolerance to pressure, allowing it to dive deeper without getting crushed by pressure. +- Deep Diver subs can dive 20% deeper than other subs without getting crushed. +- Added mineral scanning functionality for sonars: enabled by default for handheld sonars, can be enabled for other sonar devices in the Sub Editor with Sonar component's "HasMineralScanner" property +- Added concatenation component (a signal component that joins two inputs together). - Added a ready check that can be used to check if everyone is ready to depart from an outpost in multiplayer. - Added "ignore" order that can be used to prevent bots from using/repairing/taking specific items or devices. - Discharge coil's range can be visualized by holding space while one is selected in the sub editor. @@ -122,24 +415,74 @@ Additions and changes: - Improved turret range visualization in the sub editor. - Made skill checks in outpost events probability-based: a low skill doesn't mean you'll always fail, just that you're less likely to succeed. - Reworked bonethresher's behavior and attacks. Tigerthreshers now protect Bonethreshers with a low priority. Minor tweaks to the ragdolls and animations. -- Diving suits the bots have dropped in an outpost are automatically moved to the sub when departing from the outpost. -- Outpost security allows "stealing" diving masks and suits if the outpost is flooding. - Added water percentage output to water detector. +- Reduced scrap spawn rates in wrecks. +- Removed the explosive cargo mission variant where one of the explosives spontaneously explodes. +- Split combat missions into a separate game mode. +- Added a toggle for transparent wiring mode. +- The "Leaving Start Location" track isn't played at outposts. +- Increased default KillDisconnectTime to 5 minutes. +- Added pet food item. +- Added support for defining sonar icon colors in XML. +- Neutralize ballasts in submarine test level. +- Items can be attached to level walls. +- Fixed watcher's gaze causing severe effects inside the submarine. +- UI: Moved the navigation controls to the right side of the sonar view and readjusted the layout. +- Moved the cleanup order to the last in the maintenance category. +- Flagged certain parts of the nose and tail of the vanilla submarines as non-targetable, because the monsters tended to go inside the nose/tail parts too often. +- Increased the sound ranges for Tigerthresher, Leucocyte, Terminal cell, Mudraptor, and Crawler (should make them more audible). +- Crawlers, Tigerthershers, and Spinelings now avoid being killed by the engine (more or less). +- Adjusted the avoiding behavior for monsters. + +Bots: +- Bots don't anymore clean up other diving suits when they have one equipped already. +- Bots don't anymore take diving suits off inside outposts (unless they have to). +- Fixed bots getting stuck on ladders when their body is near the floor. +- Fixed bots equipping diving gear too eagerly when the oxygen level drops in the room. +- Fixed all npcs and bots using the "Passive" idle behavior. Changed the guard idling so that they now more and prefer longer distances. Also other crew members should now move slightly more than previously. +- Increase combat priority of some tools so that the bots prefer those to toy hammer. +- More descriptive bot dialog when they can't find items they're looking for. +- Diving suits the bots have dropped in an outpost are automatically moved to the cargo bay when departing from the outpost. +- Outpost security allows "stealing" diving masks and suits if the outpost is flooding. +- Bots keep more distance to the player while following underwater and outside the sub. +- Fixed a rare crash in AIObjectiveIdle.Wander method. +- Bots now defend themselves (if possible) also when they are being attacked outside of the submarine. +- Bots should no longer hoard fuel rods. +- Fixed bots getting stuck while swimming near the submarine, because they kept switching between different steering modes. +- Fixed bots not avoiding other submarines connected to the submarine they are heading to while swimming around the submarine using waypoints. +- Fixed broken walls near hatches/doors sometimes preventing characters from entering the sub/outpost through the hatch/door. +- Fixed security or bots that have been ordered to fight enemies first fleeing from the enemies. +- Fixed bots sometimes failing to put out fires in multi-hull rooms. +- Fixed bots not being able to clean up items that occupy both hands (like the fire extinguisher). +- Fixed bots having issues with empty items while operating the reactor or the turrets, causing them e.g. to not knowing how to load the target item. +- Fixed outpost NPCs sometimes "cleaning up" the spawned toolbox in the event "clownrelations1". +- Fixed bots loading more rods to the reactor when the load is too high, even if the current amount of fuel is already enough to maximize the output. + +Modding: +- Added "IgnoreWhileInside" and "IgnoreWhileOutside" parameters on character targeting parameters. +- Added ranged projectile attacks for monsters. See Spineling for an example. Note that there are five different rotation modes for the projectile aiming: Fixed, Target, Limb, MainLimb, and Collider. +- Added a "sweep attack", which makes the creature sinuate while closing to the target instead of moving straight towards it. Used on Spineling. +- Added limb hiding (permanent or temporary) with status effects. Used on Spineling. +- Added limb breaking with status effects. +- Allow characters to move full speed after attacking, when the cooldown is active. The property is found in the attack definition. +- Allow to use the idle behavior (wandering) after attacking, during the cooldown. +- Added "OnlyOutside" and "OnlyInside" attributes for status effects. Affects only the targets of the effect. +- Fixed particle's "LoopAnim" property doing nothing. Bugfixes: +- Fixed occasional "missing entity" errors caused by the server failing to write an ID card's data in a network message. +- Fixed a bug that sometimes caused power to desync in multiplayer: when connecting the second end of a wire to a device other than a junction box, the server would sometimes not register the wire as being connected. - Fixed previous messages disappearing from terminals and logbooks when transitioning to a new level in the campaign. - Fixed sprite depths in dockingmodule 2, should prevent z-fighting. -- Fixed bots getting stuck while swimming near the submarine, because they kepth switching between different steering modes. -- Fixed bots not avoiding other submarines connected to the submarine they are heading to while swimming around the submarine using waypoints. +- Fixed crashing when a bot abandons AIObjectiveCombat due to the target being in a different sub (e.g. if a character the outpost security is chasing moves from the outpost to the sub). - Fixed undocking enabling all disabled nodes instead of just those that were connected to the docking port in question. - Fixed EventManager to always choosing the same events from identical event sets in a given level. Meaning that if a level for example had 3 monster spawns that spawn either a crawler or a mudraptor, and the first event spawned a crawler, the rest would as well. In practice this lead to there being less variation in monster spawns than intended. -- Kastrull: Added more waypoints around the drone so that bots know how to get around it when it's docked to the main sub. +- Kastrull: Added more waypoints around the drone so that bots know how to get around it when it's docked to the main sub. Also fixed the airlock waypoints not being linked to the doors, causing the bots not being able to operate them. - Fixed Health Scanner HUD showing a disguised character's true identity. - Fixed health interface showing the original face and occupation of disguised characters. - Fixed changelog layout getting messed up in the main menu after changing the resolution. - Fixed some structures turning into wrecked versions when reloading the core content package with the "reloadcorepackage" console command. - Fixed vent output being calculated incorrectly in multi-hull rooms. -- Fixed broken walls near hatches/doors sometimes preventing characters from entering the sub/outpost through the hatch/door. - Fixed switch state being toggled when selecting them in the sub editor's wiring mode. - Fixed engine not being affected by low power unless the voltage is low enough to turn it off completely (in practice meaning that there's not benefit to supplying the engine more than 50% of it's power consumption). - Fixed characters getting healed between campaign rounds in single player. @@ -152,7 +495,6 @@ Bugfixes: - Fixed reputation loss when "stealing" fire extinguishers from outposts when there's a fire. - Fixed periscopes outputting rotation values incorrectly when connected to something else than a turret (e.g. camera). - Fixed resetting game settings reloading content packages, causing items to disappear if the settings are reset when a round is running. -- Fixed crashing when a bot abandons AIObjectiveCombat due to the target being in a different sub (e.g. if a character the outpost security is chasing moves from the outpost to the sub). - Fixed pet's hunger/happiness values and inventories not getting saved between rounds. - Fixed pet name tags disappearing client-side between rounds. - Fixed empty oxygen tank not triggering the warning sound of a diving suit's oxygen supply. @@ -162,6 +504,46 @@ Bugfixes: - Fixed zoom getting stuck whenever exiting a railgun, coilgun or periscope and instantly hovering on an inventory slot. - Fixed turret range upgrades not increasing the range of the turret's spotlight. - Fixed all monsters spawned by the same monster event having the same sets of items. +- Fixed disconnected, hanging wires sometimes appearing at the wrong end of the wire. +- Fixed progress bar saying "welding" instead of "cutting" when cutting open a welded door. +- Fixed explosion damage to items not being diminished if there are obstacles between the explosion and the item. +- Fixes to flamer particles going through walls. +- Fixed items that are included in multiple categories (e.g. oxygen tanks, battery cells) not appearing in the sub editor's entity list unless using the search bar. +- Fixed psychosis sounds affecting all players. +- Fixed fabricators and deconstructors deteriorating even if they're not running. +- Fixed crashing when selecting a fabricator linked to a deconstructor or vice versa. +- Fixed "hide offensive server names" tickbox working the wrong way around in the server browser. +- Fixed console errors when setting an engine's max force to 0. +- Fixed in ability to damage huskified crew members if friendly fire is disabled. +- Fixed prototype steam cannon particles going through walls. +- Fixed "engineers are special" outpost event not giving XP when successfully helping the NPC. +- Fixed "propaganda" and "clown outbreak" outpost events not triggering. +- Fixed wires not appearing in some item assemblies placed in the sub editor until the sub is saved. +- Fixed decorative sprites being positioned incorrectly on mirrored items (such as pumps and fabricators). +- Fixed rotation limits being incorrect on turrets that have been mirrored before saving the sub. +- Fixed crashing when a bot is left to idle inside ruins. +- Fixed deconstructing an SMG magazine causing an SMG round to drop on the ground. +- Fixed empty SMG magazines not deconstructing to plastic. +- Fixed SMG magazines in character's inventories spawning SMG rounds at the start of a round (again). +- Fixed console errors in StatusEffect.GetPosition when applying a delayed status effect on a removed character. +- Fixed the latching behavior on Crawlers. +- Fixed latched creatures not releasing the sub when the submarine moves fast enough (defined in the latching behavior definition). +- Fixed outpost events sometimes triggering on dead/unconscious players. +- Fixed wifi component accepting input from chat regardless of the "link to chat" setting (the setting only determined if the component outputs the messages to the chat of the player holding the item, making it only useful for headsets). +- Fixed an issue that caused freezes when opening the server browser. +- Fixed "there is not enough room in the input inventory" error when placing a legacy medical fabricator in the sub editor. +- Fixed picking up a captain's pipe selecting it, preventing aiming until the item is deselected. +- Fixed hitscan projectiles briefly dropping out of the weapon client-side when fired. +- Fixed inability to play the campaign from the same local network when hosting with the dedicated server. +- Fixed crashing when clicking "spectate" in the server lobby after connection has been lost. +- Fixed occasional "too much data in network event" error messages. +- Fixed clients assigning different initial location reputations than the server, causing the round summary to display random reputation loss/gain when leaving the first outpost. +- Fixed "No AI Target" property not working properly. +- Fixed a rare crash when crawlers latch to a submarine. +- Fixed the vanilla submarines not always having any wrenches, which causes the bots not being able to fix mechanical items if they don't have a wrench in their inventory. Also made sure that all engineers have a wrench when they spawn. +- Fixed recovering and repairing a shuttle not removing the wall damage client-side. +- Fixed crashing when a hull smaller than 16x16px is painted or becomes dirty. +- Fixed characters not being visible when viewing an area far away from the player with a drone in multiplayer. --------------------------------------------------------------------------------------------------------- v0.10.6.2 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/Barotrauma/BarotraumaShared/serversettings.xml b/Barotrauma/BarotraumaShared/serversettings.xml index eb97bbb80..9b96f6b02 100644 --- a/Barotrauma/BarotraumaShared/serversettings.xml +++ b/Barotrauma/BarotraumaShared/serversettings.xml @@ -41,7 +41,7 @@ modeselectionmode="Manual" endvoterequiredratio="0.6" kickvoterequiredratio="0.6" - killdisconnectedtime="120" + killdisconnectedtime="300" kickafktime="600" traitoruseratio="True" traitorratio="0.2"