diff --git a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs index 17c4300d4..11e5a5bd4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs @@ -328,9 +328,9 @@ namespace Barotrauma { previousOffset = offset; } - + //how much to zoom out (zoom completely out when offset is 1000) - float zoomOutAmount = Math.Min(offset.Length() / 1000.0f, 1.0f); + float zoomOutAmount = GetZoomAmount(offset); //zoom amount when resolution is not taken into account float unscaledZoom = MathHelper.Lerp(DefaultZoom, MinZoom, zoomOutAmount); //zoom with resolution taken into account (zoom further out on smaller resolutions) @@ -409,5 +409,15 @@ namespace Barotrauma //Vector2 screenCoords = Vector2.Transform(coords, transform); return Vector2.Transform(coords, transform); } + + private float GetZoomAmount(Vector2 offset) + { + return Math.Min(offset.Length() / 1000.0f, 1.0f); + } + + public float GetZoomAmountFromPrevious() + { + return GetZoomAmount(previousOffset); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs index dd836a3c7..075153900 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/HumanAIController.cs @@ -5,8 +5,6 @@ namespace Barotrauma { partial class HumanAIController : AIController { - public static bool debugai; - partial void InitProjSpecific() { /*if (GameMain.GameSession != null && GameMain.GameSession.CrewManager != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index f35a9f860..fe09303c5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -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)) @@ -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) { @@ -810,7 +810,7 @@ namespace Barotrauma var iconStyle = GUI.Style.GetComponentStyle("CampaignInteractionBubble." + CampaignInteractionType); if (iconStyle != null) { - Vector2 headPos = AnimController.GetLimb(LimbType.Head)?.WorldPosition ?? WorldPosition + Vector2.UnitY * 100.0f; + Vector2 headPos = AnimController.GetLimb(LimbType.Head)?.body?.DrawPosition ?? DrawPosition + Vector2.UnitY * 100.0f; Vector2 iconPos = headPos; iconPos.Y = -iconPos.Y; nameColor = iconStyle.Color; @@ -835,7 +835,7 @@ namespace Barotrauma var iconStyle = GUI.Style.GetComponentStyle("PetIcon." + petStatus); if (iconStyle != null) { - Vector2 headPos = AnimController.GetLimb(LimbType.Head)?.WorldPosition ?? WorldPosition + Vector2.UnitY * 100.0f; + Vector2 headPos = AnimController.GetLimb(LimbType.Head)?.body?.DrawPosition ?? DrawPosition + Vector2.UnitY * 100.0f; Vector2 iconPos = headPos; iconPos.Y = -iconPos.Y; var icon = iconStyle.Sprites[GUIComponent.ComponentState.None].First(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index ed15b154c..e640ad831 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -11,15 +11,15 @@ namespace Barotrauma { class CharacterHUD { - private static Dictionary orderIndicatorCount = new Dictionary(); + private static readonly Dictionary orderIndicatorCount = new Dictionary(); const float ItemOverlayDelay = 1.0f; private static Item focusedItem; private static float focusedItemOverlayTimer; - private static List brokenItems = new List(); + private static readonly List brokenItems = new List(); private static float brokenItemsCheckTimer; - private static Dictionary cachedHudTexts = new Dictionary(); + private static readonly Dictionary cachedHudTexts = new Dictionary(); private static GUIFrame hudFrame; public static GUIFrame HUDFrame @@ -126,6 +126,11 @@ namespace Barotrauma { character.Inventory.Update(deltaTime, cam); } + else + { + character.Inventory.ClearSubInventories(); + } + for (int i = 0; i < character.Inventory.Items.Length - 1; i++) { var item = character.Inventory.Items[i]; @@ -199,9 +204,18 @@ namespace Barotrauma if (GameMain.GameSession?.CrewManager != null) { orderIndicatorCount.Clear(); - foreach (Pair timedOrder in GameMain.GameSession.CrewManager.ActiveOrders) + foreach (Pair activeOrder in GameMain.GameSession.CrewManager.ActiveOrders) { - DrawOrderIndicator(spriteBatch, cam, character, timedOrder.First, MathHelper.Clamp(timedOrder.Second / 10.0f, 0.2f, 1.0f)); + 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); + if (iconAlpha <= 0.0f) { continue; } + DrawOrderIndicator(spriteBatch, cam, character, activeOrder.First, iconAlpha: iconAlpha, createOffset: false, scaleMultiplier: 0.5f); + } } if (character.CurrentOrder != null) @@ -218,14 +232,18 @@ namespace Barotrauma foreach (Item brokenItem in brokenItems) { if (brokenItem.NonInteractable) { continue; } - float dist = Vector2.Distance(character.WorldPosition, brokenItem.WorldPosition); - Vector2 drawPos = brokenItem.DrawPosition; - float alpha = Math.Min((1000.0f - dist) / 1000.0f * 2.0f, 1.0f); + float alpha = GetDistanceBasedIconAlpha(brokenItem); if (alpha <= 0.0f) continue; - GUI.DrawIndicator(spriteBatch, drawPos, cam, 100.0f, GUI.BrokenIcon, + GUI.DrawIndicator(spriteBatch, brokenItem.DrawPosition, cam, 100.0f, GUI.BrokenIcon, Color.Lerp(GUI.Style.Red, GUI.Style.Orange * 0.5f, brokenItem.Condition / brokenItem.MaxCondition) * alpha); } + float GetDistanceBasedIconAlpha(ISpatialEntity target, float maxDistance = 1000.0f) + { + float dist = Vector2.Distance(character.WorldPosition, target.WorldPosition); + 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.FocusedCharacter != null && character.FocusedCharacter.CanBeSelected) @@ -362,8 +380,8 @@ namespace Barotrauma (int)(HUDLayoutSettings.BottomRightInfoArea.X + HUDLayoutSettings.BottomRightInfoArea.Width * 0.05f), (int)(HUDLayoutSettings.BottomRightInfoArea.Y + HUDLayoutSettings.BottomRightInfoArea.Height * 0.1f), (int)(HUDLayoutSettings.BottomRightInfoArea.Width / 2), - (int)(HUDLayoutSettings.BottomRightInfoArea.Height * 0.7f))); - character.Info.DrawPortrait(spriteBatch, HUDLayoutSettings.PortraitArea.Location.ToVector2(), new Vector2(-12 * GUI.Scale, 4 * GUI.Scale), targetWidth: HUDLayoutSettings.PortraitArea.Width, true); + (int)(HUDLayoutSettings.BottomRightInfoArea.Height * 0.7f)), character.Info.IsDisguisedAsAnother); + character.Info.DrawPortrait(spriteBatch, HUDLayoutSettings.PortraitArea.Location.ToVector2(), new Vector2(-12 * GUI.Scale, 4 * GUI.Scale), targetWidth: HUDLayoutSettings.PortraitArea.Width, true, character.Info.IsDisguisedAsAnother); } mouseOnPortrait = HUDLayoutSettings.BottomRightInfoArea.Contains(PlayerInput.MousePosition) && !character.ShouldLockHud(); if (mouseOnPortrait) @@ -475,19 +493,12 @@ namespace Barotrauma return character.ShouldLockHud(); } - private static void DrawOrderIndicator(SpriteBatch spriteBatch, Camera cam, Character character, Order order, float iconAlpha = 1.0f) + private static void DrawOrderIndicator(SpriteBatch spriteBatch, Camera cam, Character character, Order order, float iconAlpha = 1.0f, bool createOffset = true, float scaleMultiplier = 1.0f) { if (order?.SymbolSprite == null) { return; } + if (order.IsReport && order.OrderGiver != character && !order.HasAppropriateJob(character)) { return; } - if (order.TargetAllCharacters) - { - if (order.OrderGiver != character && !order.HasAppropriateJob(character)) - { - return; - } - } - - ISpatialEntity target = order.ConnectedController != null ? order.ConnectedController.Item : order.TargetSpatialEntity; + ISpatialEntity target = order.ConnectedController?.Item ?? order.TargetSpatialEntity; if (target == null) { return; } //don't show the indicator if far away and not inside the same sub @@ -503,7 +514,7 @@ 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); + GUI.DrawIndicator(spriteBatch, drawPos, cam, 100.0f, order.SymbolSprite, order.Color * iconAlpha, createOffset: createOffset, scaleMultiplier: scaleMultiplier); orderIndicatorCount[target] = orderIndicatorCount[target] + 1; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index e80f86d99..cf337b8e4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -6,6 +6,8 @@ using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System.Xml.Linq; +using System.IO; +using Barotrauma.Items.Components; namespace Barotrauma { @@ -15,6 +17,12 @@ namespace Barotrauma public bool LastControlled; + private Sprite disguisedPortrait; + private List disguisedAttachmentSprites; + private Vector2? disguisedSheetIndex; + private Sprite disguisedJobIcon; + private Color disguisedJobColor; + public static void Init() { infoAreaPortraitBG = GUI.Style.GetComponentStyle("InfoAreaPortraitBG")?.GetDefaultSprite(); @@ -175,6 +183,195 @@ namespace Barotrauma } } + private void GetDisguisedSprites(IdCard idCard) + { + if (idCard.StoredJobPrefab == null || idCard.StoredPortrait == null) + { + string[] readTags = idCard.Item.Tags.Split(','); + + if (idCard.StoredJobPrefab == null) + { + string jobIdTag = readTags.First(s => s.StartsWith("jobid:")); + + if (jobIdTag != string.Empty && jobIdTag.Length > 6) + { + string jobId = jobIdTag.Substring(6); + if (jobId != string.Empty) + { + idCard.StoredJobPrefab = JobPrefab.Get(jobId); + } + } + } + + if (idCard.StoredPortrait == null) + { + string disguisedGender = string.Empty; + string disguisedRace = string.Empty; + string disguisedHeadSpriteId = string.Empty; + int disguisedHairIndex = -1; + int disguisedBeardIndex = -1; + int disguisedMoustacheIndex = -1; + int disguisedFaceAttachmentIndex = -1; + + foreach (string tag in readTags) + { + string[] s = tag.Split(':'); + + switch (s[0]) + { + case "gender": + disguisedGender = s[1]; + break; + + case "race": + disguisedRace = s[1]; + break; + + case "headspriteid": + disguisedHeadSpriteId = s[1]; + break; + + case "hairindex": + disguisedHairIndex = int.Parse(s[1]); + break; + + case "beardindex": + disguisedBeardIndex = int.Parse(s[1]); + break; + + case "moustacheindex": + disguisedMoustacheIndex = int.Parse(s[1]); + break; + + case "faceattachmentindex": + disguisedFaceAttachmentIndex = int.Parse(s[1]); + break; + + case "sheetindex": + string[] vectorValues = s[1].Split(";"); + idCard.StoredSheetIndex = new Vector2(float.Parse(vectorValues[0]), float.Parse(vectorValues[1])); + break; + } + } + + if (disguisedGender == string.Empty || disguisedRace == string.Empty || disguisedHeadSpriteId == string.Empty) + { + idCard.StoredPortrait = null; + idCard.StoredAttachments = null; + return; + } + + foreach (XElement limbElement in Ragdoll.MainElement.Elements()) + { + if (!limbElement.GetAttributeString("type", "").Equals("head", StringComparison.OrdinalIgnoreCase)) { continue; } + + XElement spriteElement = limbElement.Element("sprite"); + if (spriteElement == null) { continue; } + + string spritePath = spriteElement.Attribute("texture").Value; + + spritePath = spritePath.Replace("[GENDER]", disguisedGender); + spritePath = spritePath.Replace("[RACE]", disguisedRace.ToLowerInvariant()); + spritePath = spritePath.Replace("[HEADID]", disguisedHeadSpriteId); + + string fileName = Path.GetFileNameWithoutExtension(spritePath); + + //go through the files in the directory to find a matching sprite + foreach (string file in Directory.GetFiles(Path.GetDirectoryName(spritePath))) + { + if (!file.EndsWith(".png", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + string fileWithoutTags = Path.GetFileNameWithoutExtension(file); + fileWithoutTags = fileWithoutTags.Split('[', ']').First(); + if (fileWithoutTags != fileName) { continue; } + idCard.StoredPortrait = new Sprite(spriteElement, "", file) { RelativeOrigin = Vector2.Zero }; + break; + } + + break; + } + + if (Wearables != null) + { + XElement disguisedHairElement, disguisedBeardElement, disguisedMoustacheElement, disguisedFaceAttachmentElement; + List disguisedHairs, disguisedBeards, disguisedMoustaches, disguisedFaceAttachments; + + Gender disguisedGenderEnum = disguisedGender == "female" ? Gender.Female : Gender.Male; + Race disguisedRaceEnum = (Race)Enum.Parse(typeof(Race), disguisedRace); + int headSpriteId = int.Parse(disguisedHeadSpriteId); + + float commonness = disguisedGenderEnum == Gender.Female ? 0.05f : 0.2f; + disguisedHairs = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables, disguisedGenderEnum, disguisedRaceEnum), WearableType.Hair, headSpriteId), WearableType.Hair, commonness); + disguisedBeards = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables, disguisedGenderEnum, disguisedRaceEnum), WearableType.Beard, headSpriteId), WearableType.Beard); + disguisedMoustaches = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables, disguisedGenderEnum, disguisedRaceEnum), WearableType.Moustache, headSpriteId), WearableType.Moustache); + disguisedFaceAttachments = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables, disguisedGenderEnum, disguisedRaceEnum), WearableType.FaceAttachment, headSpriteId), WearableType.FaceAttachment); + + if (IsValidIndex(disguisedHairIndex, disguisedHairs)) + { + disguisedHairElement = disguisedHairs[disguisedHairIndex]; + } + else + { + disguisedHairElement = GetRandomElement(disguisedHairs); + } + if (IsValidIndex(disguisedBeardIndex, disguisedBeards)) + { + disguisedBeardElement = disguisedBeards[disguisedBeardIndex]; + } + else + { + disguisedBeardElement = GetRandomElement(disguisedBeards); + } + + if (IsValidIndex(disguisedMoustacheIndex, disguisedMoustaches)) + { + disguisedMoustacheElement = disguisedMoustaches[disguisedMoustacheIndex]; + } + else + { + disguisedMoustacheElement = GetRandomElement(disguisedMoustaches); + } + if (IsValidIndex(disguisedFaceAttachmentIndex, disguisedFaceAttachments)) + { + disguisedFaceAttachmentElement = disguisedFaceAttachments[disguisedFaceAttachmentIndex]; + } + else + { + disguisedFaceAttachmentElement = GetRandomElement(disguisedFaceAttachments); + } + + idCard.StoredAttachments = new List(); + + disguisedFaceAttachmentElement?.Elements("sprite").ForEach(s => idCard.StoredAttachments.Add(new WearableSprite(s, WearableType.FaceAttachment))); + disguisedBeardElement?.Elements("sprite").ForEach(s => idCard.StoredAttachments.Add(new WearableSprite(s, WearableType.Beard))); + disguisedMoustacheElement?.Elements("sprite").ForEach(s => idCard.StoredAttachments.Add(new WearableSprite(s, WearableType.Moustache))); + disguisedHairElement?.Elements("sprite").ForEach(s => idCard.StoredAttachments.Add(new WearableSprite(s, WearableType.Hair))); + + if (OmitJobInPortraitClothing) + { + JobPrefab.NoJobElement?.Element("PortraitClothing")?.Elements("sprite").ForEach(s => idCard.StoredAttachments.Add(new WearableSprite(s, WearableType.JobIndicator))); + } + else + { + idCard.StoredJobPrefab?.ClothingElement?.Elements("sprite").ForEach(s => idCard.StoredAttachments.Add(new WearableSprite(s, WearableType.JobIndicator))); + } + } + } + } + + if (idCard.StoredJobPrefab != null) + { + disguisedJobIcon = idCard.StoredJobPrefab.Icon; + disguisedJobColor = idCard.StoredJobPrefab.UIColor; + } + + disguisedPortrait = idCard.StoredPortrait; + disguisedSheetIndex = idCard.StoredSheetIndex; + disguisedAttachmentSprites = idCard.StoredAttachments; + } + partial void LoadAttachmentSprites(bool omitJob) { if (attachmentSprites == null) @@ -219,23 +416,42 @@ namespace Barotrauma HUDLayoutSettings.BottomRightInfoArea.Height / (float)infoAreaPortraitBG.SourceRect.Height)); } - public void DrawPortrait(SpriteBatch spriteBatch, Vector2 screenPos, Vector2 offset, float targetWidth, bool flip = false) + public void DrawPortrait(SpriteBatch spriteBatch, Vector2 screenPos, Vector2 offset, float targetWidth, bool flip = false, bool evaluateDisguise = false) { - if (Portrait != null) + if (evaluateDisguise && IsDisguised) return; + + Vector2? sheetIndex; + Sprite portraitToDraw; + List attachmentsToDraw; + + if (!IsDisguisedAsAnother || !evaluateDisguise) + { + sheetIndex = Head.SheetIndex; + portraitToDraw = Portrait; + attachmentsToDraw = AttachmentSprites; + } + else + { + sheetIndex = disguisedSheetIndex; + portraitToDraw = disguisedPortrait; + attachmentsToDraw = disguisedAttachmentSprites; + } + + if (portraitToDraw != null) { // Scale down the head sprite 10% float scale = targetWidth * 0.9f / Portrait.size.X; - if (Head.SheetIndex.HasValue) + if (sheetIndex.HasValue) { - Portrait.SourceRect = new Rectangle(CalculateOffset(Portrait, Head.SheetIndex.Value.ToPoint()), Portrait.SourceRect.Size); + portraitToDraw.SourceRect = new Rectangle(CalculateOffset(portraitToDraw, sheetIndex.Value.ToPoint()), portraitToDraw.SourceRect.Size); } - Portrait.Draw(spriteBatch, screenPos + offset, Color.White, Portrait.Origin, scale: scale, spriteEffect: flip ? SpriteEffects.FlipHorizontally : SpriteEffects.None); - if (AttachmentSprites != null) + portraitToDraw.Draw(spriteBatch, screenPos + offset, Color.White, portraitToDraw.Origin, scale: scale, spriteEffect: flip ? SpriteEffects.FlipHorizontally : SpriteEffects.None); + if (attachmentsToDraw != null) { float depthStep = 0.000001f; - foreach (var attachment in AttachmentSprites) + foreach (var attachment in attachmentsToDraw) { - DrawAttachmentSprite(spriteBatch, attachment, Portrait, screenPos + offset, scale, depthStep, flip ? SpriteEffects.FlipHorizontally : SpriteEffects.None); + DrawAttachmentSprite(spriteBatch, attachment, portraitToDraw, sheetIndex, screenPos + offset, scale, depthStep, flip ? SpriteEffects.FlipHorizontally : SpriteEffects.None); depthStep += depthStep; } } @@ -258,30 +474,34 @@ namespace Barotrauma float depthStep = 0.000001f; foreach (var attachment in AttachmentSprites) { - DrawAttachmentSprite(spriteBatch, attachment, headSprite, screenPos, scale, depthStep); + DrawAttachmentSprite(spriteBatch, attachment, headSprite, Head.SheetIndex, screenPos, scale, depthStep); depthStep += depthStep; } } } } - public void DrawJobIcon(SpriteBatch spriteBatch, Vector2 pos, float scale = 1.0f) + public void DrawJobIcon(SpriteBatch spriteBatch, Vector2 pos, float scale = 1.0f, bool evaluateDisguise = false) { - var icon = Job?.Prefab?.Icon; + if (evaluateDisguise && IsDisguised) return; + var icon = !IsDisguisedAsAnother || !evaluateDisguise ? Job?.Prefab?.Icon : disguisedJobIcon; if (icon == null) { return; } - icon.Draw(spriteBatch, pos, Job.Prefab.UIColor, scale: scale); - } - public void DrawJobIcon(SpriteBatch spriteBatch, Rectangle area) - { - var icon = Job?.Prefab?.Icon; - if (icon == null) { return; } - icon.Draw(spriteBatch, - area.Center.ToVector2(), - Job.Prefab.UIColor, - scale: Math.Min(area.Width / (float)icon.SourceRect.Width, area.Height / (float)icon.SourceRect.Height)); + Color iconColor = !IsDisguisedAsAnother || !evaluateDisguise ? Job.Prefab.UIColor : disguisedJobColor; + + icon.Draw(spriteBatch, pos, iconColor, scale: scale); } - private void DrawAttachmentSprite(SpriteBatch spriteBatch, WearableSprite attachment, Sprite head, Vector2 drawPos, float scale, float depthStep, SpriteEffects spriteEffects = SpriteEffects.None) + public void DrawJobIcon(SpriteBatch spriteBatch, Rectangle area, bool evaluateDisguise = false) + { + if (evaluateDisguise && IsDisguised) return; + var icon = !IsDisguisedAsAnother || !evaluateDisguise ? Job?.Prefab?.Icon : disguisedJobIcon; + if (icon == null) { return; } + Color iconColor = !IsDisguisedAsAnother || !evaluateDisguise ? Job.Prefab.UIColor : disguisedJobColor; + + icon.Draw(spriteBatch, area.Center.ToVector2(), iconColor, scale: Math.Min(area.Width / (float)icon.SourceRect.Width, area.Height / (float)icon.SourceRect.Height)); + } + + private void DrawAttachmentSprite(SpriteBatch spriteBatch, WearableSprite attachment, Sprite head, Vector2? sheetIndex, Vector2 drawPos, float scale, float depthStep, SpriteEffects spriteEffects = SpriteEffects.None) { if (attachment.InheritSourceRect) { @@ -289,9 +509,9 @@ namespace Barotrauma { attachment.Sprite.SourceRect = new Rectangle(CalculateOffset(head, attachment.SheetIndex.Value), head.SourceRect.Size); } - else if (Head.SheetIndex.HasValue) + else if (sheetIndex.HasValue) { - attachment.Sprite.SourceRect = new Rectangle(CalculateOffset(head, Head.SheetIndex.Value.ToPoint()), head.SourceRect.Size); + attachment.Sprite.SourceRect = new Rectangle(CalculateOffset(head, sheetIndex.Value.ToPoint()), head.SourceRect.Size); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index 6fc7e4290..e02363bf8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -416,8 +416,7 @@ namespace Barotrauma Character character = null; if (noInfo) { - character = Create(speciesName, position, seed, null, false); - character.ID = id; + character = Create(speciesName, position, seed, characterInfo: null, id: id, isRemotePlayer: false); bool containsStatusData = inc.ReadBoolean(); if (containsStatusData) { @@ -434,8 +433,7 @@ namespace Barotrauma CharacterInfo info = CharacterInfo.ClientRead(infoSpeciesName, inc); - character = Create(speciesName, position, seed, info, ownerId > 0 && GameMain.Client.ID != ownerId, hasAi); - character.ID = id; + character = Create(speciesName, position, seed, characterInfo: info, id: id, isRemotePlayer: ownerId > 0 && GameMain.Client.ID != ownerId, hasAi: hasAi); character.TeamID = (TeamType)teamID; character.CampaignInteractionType = (CampaignMode.InteractionType)inc.ReadByte(); if (character.CampaignInteractionType != CampaignMode.InteractionType.None) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index 96b3eb799..6b855f999 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -53,7 +53,7 @@ namespace Barotrauma private GUIFrame healthWindow; - private GUIComponent deadIndicator; + private GUITextBlock deadIndicator; private GUIComponent lowSkillIndicator; @@ -225,13 +225,14 @@ namespace Barotrauma Character.Controlled.ResetInteract = true; if (openHealthWindow != null) { - if (value.Character.Info == null || Character.Controlled.HasEquippedItem("healthscanner")) + if (value.Character.Info == null || value.Character == Character.Controlled || Character.Controlled.HasEquippedItem("healthscanner")) { openHealthWindow.characterName.Text = value.Character.Name; } else { openHealthWindow.characterName.Text = value.Character.Info.DisplayName; + value.Character.Info.CheckDisguiseStatus(false); } if (Character.Controlled.SelectedConstruction != null && Character.Controlled.SelectedConstruction.GetComponent() == null) @@ -330,11 +331,19 @@ namespace Barotrauma } ); deadIndicator = new GUITextBlock(new RectTransform(new Vector2(0.9f, 0.1f), limbSelection.RectTransform, Anchor.Center), - text: TextManager.Get("Deceased"), font: GUI.LargeFont, textAlignment: Alignment.Center, wrap: true, style: "GUIToolTip") + text: TextManager.Get("Deceased"), font: GUI.LargeFont, textAlignment: Alignment.Center, style: "GUIToolTip") { Visible = false, CanBeFocused = false }; + if (deadIndicator.Text.Contains(' ')) + { + deadIndicator.Wrap = true; + } + else + { + deadIndicator.AutoScaleHorizontal = true; + } var rightSide = new GUIFrame(new RectTransform(new Vector2(0.4f, 1.0f), paddedHealthWindow.RectTransform), style: null); @@ -405,15 +414,17 @@ namespace Barotrauma CanBeFocused = true }; + textLayout.RectTransform.RelativeOffset = new Vector2(0, 0.025f); + var nameContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.2f), textLayout.RectTransform) { MinSize = new Point(0, 20) }, isHorizontal: true) { Stretch = true }; - new GUICustomComponent(new RectTransform(new Vector2(0.2f, 1.0f), nameContainer.RectTransform), + new GUICustomComponent(new RectTransform(new Vector2(0.2f, 1.0f), nameContainer.RectTransform, Anchor.CenterLeft), onDraw: (spriteBatch, component) => { - character.Info.DrawPortrait(spriteBatch, new Vector2(component.Rect.X, component.Rect.Center.Y - component.Rect.Width / 2), Vector2.Zero, component.Rect.Width); + character.Info.DrawPortrait(spriteBatch, new Vector2(component.Rect.X, component.Rect.Center.Y - component.Rect.Width / 2), Vector2.Zero, component.Rect.Width, false, openHealthWindow?.Character != Character.Controlled); }); characterName = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), nameContainer.RectTransform), "", textAlignment: Alignment.CenterLeft, font: GUI.SubHeadingFont) { @@ -422,7 +433,7 @@ namespace Barotrauma new GUICustomComponent(new RectTransform(new Vector2(0.2f, 1.0f), nameContainer.RectTransform), onDraw: (spriteBatch, component) => { - character.Info.DrawJobIcon(spriteBatch, component.Rect); + character.Info.DrawJobIcon(spriteBatch, component.Rect, openHealthWindow?.Character != Character.Controlled); }); @@ -951,6 +962,28 @@ namespace Barotrauma SuicideButton.Visible = Character == Character.Controlled && !Character.IsDead && Character.IsIncapacitated; + if (GameMain.GameSession?.Campaign is { } campaign) + { + RectTransform endRoundButton = campaign?.EndRoundButton?.RectTransform; + RectTransform readyCheckButton = campaign?.ReadyCheckButton?.RectTransform; + if (endRoundButton != null) + { + if (SuicideButton.Visible) + { + Point offset = new Point(0, SuicideButton.Rect.Height); + endRoundButton.ScreenSpaceOffset = offset; + } + else if (endRoundButton.ScreenSpaceOffset != Point.Zero) + { + endRoundButton.ScreenSpaceOffset = Point.Zero; + } + if (readyCheckButton != null) + { + readyCheckButton.ScreenSpaceOffset = endRoundButton.ScreenSpaceOffset; + } + } + } + cprButton.Visible = Character == Character.Controlled?.SelectedCharacter && (Character.IsUnconscious || Character.Stun > 0.0f) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs index f452ddcb2..7581cd84d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs @@ -328,8 +328,7 @@ namespace Barotrauma deformation = ragdoll.Limbs .Where(l => l != null) .SelectMany(l => l.Deformations) - .Where(d => d.TypeName == typeName && d.Sync == sync) - .FirstOrDefault(); + .FirstOrDefault(d => d.TypeName == typeName && d.Sync == sync); } if (deformation == null) { @@ -971,11 +970,11 @@ namespace Barotrauma XElement element; if (random) { - element = info.FilterByTypeAndHeadID(character.Info.FilterElementsByGenderAndRace(character.Info.Wearables), type)?.GetRandom(Rand.RandSync.ClientOnly); + element = info.FilterByTypeAndHeadID(info.FilterElementsByGenderAndRace(info.Wearables, info.Gender, info.Race), type, info.Head.HeadSpriteId)?.GetRandom(Rand.RandSync.ClientOnly); } else { - element = info.FilterByTypeAndHeadID(character.Info.FilterElementsByGenderAndRace(character.Info.Wearables), type)?.FirstOrDefault(); + element = info.FilterByTypeAndHeadID(info.FilterElementsByGenderAndRace(info.Wearables, info.Gender, info.Race), type, info.Head.HeadSpriteId)?.FirstOrDefault(); } if (element != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index d9a19a6ec..5992eab99 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -491,7 +491,11 @@ namespace Barotrauma { SteamManager.NetworkingDebugLog = !SteamManager.NetworkingDebugLog; SteamManager.SetSteamworksNetworkingDebugLog(SteamManager.NetworkingDebugLog); + })); + commands.Add(new Command("readycheck", "Commence a ready check in multiplayer.", (string[] args) => + { + NewMessage("Ready checks can only be commenced in multiplayer.", Color.Red); })); AssignRelayToServer("kick", false); @@ -527,6 +531,7 @@ namespace Barotrauma AssignRelayToServer("traitorlist", true); AssignRelayToServer("money", true); AssignRelayToServer("setskill", true); + AssignRelayToServer("readycheck", true); AssignOnExecute("control", (string[] args) => { @@ -547,14 +552,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) => @@ -2231,6 +2237,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/Missions/BeaconMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/BeaconMission.cs new file mode 100644 index 000000000..167e254de --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/BeaconMission.cs @@ -0,0 +1,15 @@ +using Barotrauma.Networking; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Barotrauma +{ + partial class BeaconMission : Mission + { + public override void ClientReadInitial(IReadMessage msg) + { + return; + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CombatMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CombatMission.cs index 8bda0040d..2c1ab95a6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CombatMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/CombatMission.cs @@ -2,7 +2,7 @@ namespace Barotrauma { - partial class CombatMission + partial class CombatMission : Mission { public override string Description { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs new file mode 100644 index 000000000..b8e18a7c3 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs @@ -0,0 +1,56 @@ +using Barotrauma.Extensions; +using Barotrauma.Items.Components; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + partial class MineralMission : Mission + { + public override void ClientReadInitial(IReadMessage msg) + { + for (int i = 0; i < ResourceClusters.Count; i++) + { + var amount = msg.ReadByte(); + var rotation = msg.ReadSingle(); + for (int j = 0; j < amount; j++) + { + var item = Item.ReadSpawnData(msg); + if (item.GetComponent() is Holdable h) + { + h.AttachToWall(); + item.Rotation = rotation; + } + if (SpawnedResources.TryGetValue(item.Prefab.Identifier, out var resources)) + { + resources.Add(item); + } + else + { + SpawnedResources.Add(item.Prefab.Identifier, new List() { item }); + } + } + } + + CalculateMissionClusterPositions(); + + for(int i = 0; i < ResourceClusters.Count; i++) + { + var identifier = msg.ReadString(); + var count = msg.ReadByte(); + var resources = new Item[count]; + for (int j = 0; j < count; j++) + { + var id = msg.ReadUInt16(); + var entity = Entity.FindEntityByID(id); + if (!(entity is Item item)) { continue; } + resources[j] = item; + } + RelevantLevelResources.Add(identifier, resources); + } + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs index a9b5421c7..ccdcb04da 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; namespace Barotrauma { - partial class Mission + abstract partial class Mission { partial void ShowMessageProjSpecific(int missionState) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionMode.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionMode.cs index 788849362..24e8e3db5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionMode.cs @@ -3,7 +3,7 @@ using System; namespace Barotrauma { - partial class MissionMode : GameMode + abstract partial class MissionMode : GameMode { public override void ShowStartMessage() { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/NestMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/NestMission.cs new file mode 100644 index 000000000..b8f29883f --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/NestMission.cs @@ -0,0 +1,26 @@ +using Barotrauma.Networking; +using FarseerPhysics; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + partial class NestMission : Mission + { + public override void ClientReadInitial(IReadMessage msg) + { + nestPosition = new Vector2( + msg.ReadSingle(), + msg.ReadSingle()); + ushort itemCount = msg.ReadUInt16(); + for (int i = 0; i < itemCount; i++) + { + var item = Item.ReadSpawnData(msg); + items.Add(item); + if (item.body != null) + { + item.body.FarseerBody.BodyType = BodyType.Kinematic; + } + } + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index 3b0cea052..3c92b6bf4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -1256,17 +1256,26 @@ namespace Barotrauma #region Element drawing - public static void DrawIndicator(SpriteBatch spriteBatch, Vector2 worldPosition, Camera cam, float hideDist, Sprite sprite, Color color) + + /// 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) { Vector2 diff = worldPosition - cam.WorldViewCenter; float dist = diff.Length(); - float symbolScale = Math.Min(64.0f / sprite.size.X, 1.0f); + float symbolScale = Math.Min(64.0f / sprite.size.X, 1.0f) * scaleMultiplier; if (dist > hideDist) { float alpha = Math.Min((dist - hideDist) / 100.0f, 1.0f); - Vector2 targetScreenPos = cam.WorldToScreen(worldPosition); + Vector2 targetScreenPos = cam.WorldToScreen(worldPosition); + + if (!createOffset) + { + sprite.Draw(spriteBatch, targetScreenPos, color * alpha, rotate: 0.0f, scale: symbolScale); + return; + } + float screenDist = Vector2.Distance(cam.WorldToScreen(cam.WorldViewCenter), targetScreenPos); float angle = MathUtils.VectorToAngle(diff); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs index 78af6642a..337e98bc8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; @@ -218,6 +219,12 @@ namespace Barotrauma GUI.Style.ButtonPulse.Draw(spriteBatch, expandRect, ToolBox.GradientLerp(pulseExpand, Color.White, Color.White, Color.Transparent)); } + + if (UserData is string s && s == "ReadyCheckButton" && ReadyCheck.lastReadyCheck > DateTime.Now) + { + float progress = (ReadyCheck.lastReadyCheck - DateTime.Now).Seconds / 60.0f; + Frame.Color = ToolBox.GradientLerp(progress, Color.White, GUI.Style.Red); + } } protected override void Update(float deltaTime) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs index e6bda9377..9b4584110 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs @@ -3,6 +3,7 @@ using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using System.Linq; +using Barotrauma.Networking; namespace Barotrauma { @@ -20,7 +21,8 @@ namespace Barotrauma public enum Type { Default, - InGame + InGame, + Vote } public List Buttons { get; private set; } = new List(); @@ -47,6 +49,9 @@ namespace Barotrauma Icon.Color = value; } } + + public bool Draggable { get; set; } + public Vector2 DraggingPosition = Vector2.Zero; public GUIImage BackgroundIcon { get; private set; } private GUIImage newBackgroundIcon; @@ -60,7 +65,7 @@ namespace Barotrauma private bool iconSwitching; private bool closing; - private Type type; + private readonly Type type; public static GUIComponent VisibleBox => MessageBoxes.LastOrDefault(); @@ -97,12 +102,25 @@ namespace Barotrauma }; } - InnerFrame = new GUIFrame(new RectTransform(new Point(width, height), RectTransform, type == Type.InGame ? Anchor.TopCenter : Anchor.Center) { IsFixedSize = false }, style: null); + Anchor anchor = type switch + { + Type.InGame => Anchor.TopCenter, + Type.Vote => Anchor.TopRight, + _ => Anchor.Center + }; + + InnerFrame = new GUIFrame(new RectTransform(new Point(width, height), RectTransform, anchor) { IsFixedSize = false }, style: null); + if (type == Type.Vote) + { + int offset = GUI.IntScale(64); + InnerFrame.RectTransform.ScreenSpaceOffset = new Point(-offset, offset); + CanBeFocused = false; + } GUI.Style.Apply(InnerFrame, "", this); this.type = type; Tag = tag; - if (type == Type.Default) + if (type == Type.Default || type == Type.Vote) { Content = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.85f), InnerFrame.RectTransform, Anchor.Center)) { AbsoluteSpacing = 5 }; @@ -271,92 +289,114 @@ namespace Barotrauma protected override void Update(float deltaTime) { - if (type != Type.InGame) { return; } - - Vector2 initialPos = new Vector2(0.0f, GameMain.GraphicsHeight); - Vector2 defaultPos = new Vector2(0.0f, HUDLayoutSettings.InventoryAreaLower.Y - InnerFrame.Rect.Height - 20 * GUI.Scale); - Vector2 endPos = new Vector2(GameMain.GraphicsWidth, defaultPos.Y); - - if (!closing) + if (Draggable) { - Point step = Vector2.SmoothStep(initialPos, defaultPos, openState).ToPoint(); - InnerFrame.RectTransform.AbsoluteOffset = step; - if (BackgroundIcon != null) + if ((GUI.MouseOn == InnerFrame || InnerFrame.IsParentOf(GUI.MouseOn)) && !(GUI.MouseOn is GUIButton)) { - BackgroundIcon.RectTransform.AbsoluteOffset = new Point(InnerFrame.Rect.Location.X - (int) (BackgroundIcon.Rect.Size.X / 1.25f), (int)defaultPos.Y - BackgroundIcon.Rect.Size.Y / 2); - if (!MathUtils.NearlyEqual(openState, 1.0f)) + GUI.MouseCursor = CursorState.Move; + if (PlayerInput.PrimaryMouseButtonDown()) { - BackgroundIcon.Color = ToolBox.GradientLerp(openState, Color.Transparent, Color.White); + DraggingPosition = RectTransform.ScreenSpaceOffset.ToVector2() - PlayerInput.MousePosition; } } - if (!(Screen.Selected is RoundSummaryScreen) && !MessageBoxes.Any(mb => mb.UserData is RoundSummary)) - { - openState = Math.Min(openState + deltaTime * 2.0f, 1.0f); - } - if (GUI.MouseOn != InnerFrame && !InnerFrame.IsParentOf(GUI.MouseOn) && AutoClose) + if (PlayerInput.PrimaryMouseButtonHeld() && DraggingPosition != Vector2.Zero) { - inGameCloseTimer += deltaTime; - } - - if (inGameCloseTimer >= inGameCloseTime) - { - Close(); - } - } - else - { - openState += deltaTime * 2.0f; - Point step = Vector2.SmoothStep(defaultPos, endPos, openState - 1.0f).ToPoint(); - InnerFrame.RectTransform.AbsoluteOffset = step; - if (BackgroundIcon != null) - { - BackgroundIcon.Color *= 0.9f; - } - if (openState >= 2.0f) - { - if (Parent != null) { Parent.RemoveChild(this); } - if (MessageBoxes.Contains(this)) { MessageBoxes.Remove(this); } - } - } - - if (newBackgroundIcon != null) - { - if (!iconSwitching) - { - if (BackgroundIcon != null) - { - BackgroundIcon.Color *= 0.9f; - if (BackgroundIcon.Color.A == 0) - { - BackgroundIcon = null; - iconSwitching = true; - RemoveChild(BackgroundIcon); - } - } - else - { - iconSwitching = true; - } - iconState = 0; + GUI.MouseCursor = CursorState.Dragging; + RectTransform.ScreenSpaceOffset = (PlayerInput.MousePosition + DraggingPosition).ToPoint(); } else { - newBackgroundIcon.SetAsFirstChild(); - newBackgroundIcon.RectTransform.AbsoluteOffset = new Point(InnerFrame.Rect.Location.X - (int) (newBackgroundIcon.Rect.Size.X / 1.25f), (int)defaultPos.Y - newBackgroundIcon.Rect.Size.Y / 2); - newBackgroundIcon.Color = ToolBox.GradientLerp(iconState, Color.Transparent, Color.White); - if (newBackgroundIcon.Color.A == 255) - { - BackgroundIcon = newBackgroundIcon; - BackgroundIcon.SetAsFirstChild(); - newBackgroundIcon = null; - iconSwitching = false; - } - - iconState = Math.Min(iconState + deltaTime * 2.0f, 1.0f); + DraggingPosition = Vector2.Zero; + } + } + + if (type == Type.InGame) + { + Vector2 initialPos = new Vector2(0.0f, GameMain.GraphicsHeight); + Vector2 defaultPos = new Vector2(0.0f, HUDLayoutSettings.InventoryAreaLower.Y - InnerFrame.Rect.Height - 20 * GUI.Scale); + Vector2 endPos = new Vector2(GameMain.GraphicsWidth, defaultPos.Y); + + if (!closing) + { + Point step = Vector2.SmoothStep(initialPos, defaultPos, openState).ToPoint(); + InnerFrame.RectTransform.AbsoluteOffset = step; + if (BackgroundIcon != null) + { + BackgroundIcon.RectTransform.AbsoluteOffset = new Point(InnerFrame.Rect.Location.X - (int)(BackgroundIcon.Rect.Size.X / 1.25f), (int)defaultPos.Y - BackgroundIcon.Rect.Size.Y / 2); + if (!MathUtils.NearlyEqual(openState, 1.0f)) + { + BackgroundIcon.Color = ToolBox.GradientLerp(openState, Color.Transparent, Color.White); + } + } + if (!(Screen.Selected is RoundSummaryScreen) && !MessageBoxes.Any(mb => mb.UserData is RoundSummary)) + { + openState = Math.Min(openState + deltaTime * 2.0f, 1.0f); + } + + if (GUI.MouseOn != InnerFrame && !InnerFrame.IsParentOf(GUI.MouseOn) && AutoClose) + { + inGameCloseTimer += deltaTime; + } + + if (inGameCloseTimer >= inGameCloseTime) + { + Close(); + } + } + else + { + openState += deltaTime * 2.0f; + Point step = Vector2.SmoothStep(defaultPos, endPos, openState - 1.0f).ToPoint(); + InnerFrame.RectTransform.AbsoluteOffset = step; + if (BackgroundIcon != null) + { + BackgroundIcon.Color *= 0.9f; + } + if (openState >= 2.0f) + { + if (Parent != null) { Parent.RemoveChild(this); } + if (MessageBoxes.Contains(this)) { MessageBoxes.Remove(this); } + } + } + + if (newBackgroundIcon != null) + { + if (!iconSwitching) + { + if (BackgroundIcon != null) + { + BackgroundIcon.Color *= 0.9f; + if (BackgroundIcon.Color.A == 0) + { + BackgroundIcon = null; + iconSwitching = true; + RemoveChild(BackgroundIcon); + } + } + else + { + iconSwitching = true; + } + iconState = 0; + } + else + { + newBackgroundIcon.SetAsFirstChild(); + newBackgroundIcon.RectTransform.AbsoluteOffset = new Point(InnerFrame.Rect.Location.X - (int)(newBackgroundIcon.Rect.Size.X / 1.25f), (int)defaultPos.Y - newBackgroundIcon.Rect.Size.Y / 2); + newBackgroundIcon.Color = ToolBox.GradientLerp(iconState, Color.Transparent, Color.White); + if (newBackgroundIcon.Color.A == 255) + { + BackgroundIcon = newBackgroundIcon; + BackgroundIcon.SetAsFirstChild(); + newBackgroundIcon = null; + iconSwitching = false; + } + + iconState = Math.Min(iconState + deltaTime * 2.0f, 1.0f); + } } } - } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs index 56cf45761..db8735c88 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -707,6 +707,8 @@ namespace Barotrauma private void SortItems(GUIListBox list, SortingMethod sortingMethod) { + if (CurrentLocation == null) { return; } + if (sortingMethod == SortingMethod.AlphabeticalAsc || sortingMethod == SortingMethod.AlphabeticalDesc) { list.Content.RectTransform.SortChildren( @@ -868,13 +870,13 @@ namespace Barotrauma TextColor = Color.White * (forceDisable ? 0.5f : 1.0f), UserData = "price" }; - if(listBox == storeSellList || listBox == shoppingCrateSellList) + if (listBox == storeSellList || listBox == shoppingCrateSellList) { - priceBlock.TextGetter = () => GetCurrencyFormatted(CurrentLocation.GetAdjustedItemSellPrice(priceInfo)); + priceBlock.TextGetter = () => GetCurrencyFormatted(CurrentLocation?.GetAdjustedItemSellPrice(priceInfo) ?? 0); } else { - priceBlock.TextGetter = () => GetCurrencyFormatted(CurrentLocation.GetAdjustedItemBuyPrice(priceInfo)); + priceBlock.TextGetter = () => GetCurrencyFormatted(CurrentLocation?.GetAdjustedItemBuyPrice(priceInfo) ?? 0); } if (listBox == storeDealsList || listBox == storeBuyList || listBox == storeSellList) 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 4090a12e9..00b777e47 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -525,6 +525,7 @@ namespace Barotrauma Tutorials.Tutorial.Init(); MapGenerationParams.Init(); LevelGenerationParams.LoadPresets(); + CaveGenerationParams.LoadPresets(); OutpostGenerationParams.LoadPresets(); WreckAIConfig.LoadAll(); EventSet.LoadPrefabs(); @@ -533,6 +534,7 @@ namespace Barotrauma SkillSettings.Load(GetFilesOfType(ContentType.SkillSettings)); Order.Init(); EventManagerSettings.Init(); + BallastFloraPrefab.LoadAll(GetFilesOfType(ContentType.MapCreature)); TitleScreen.LoadState = 50.0f; yield return CoroutineStatus.Running; @@ -632,6 +634,7 @@ namespace Barotrauma /// protected override void UnloadContent() { + TextureLoader.CancelAll(); CoroutineManager.StopCoroutines("Load"); Video.Close(); VoipCapture.Instance?.Dispose(); @@ -680,7 +683,7 @@ namespace Barotrauma } public void OnLobbyJoinRequested(Steamworks.Data.Lobby lobby, Steamworks.SteamId friendId) - { + { SteamManager.JoinLobby(lobby.Id, true); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index 72234cabf..b905ecbb2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -199,7 +199,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(); @@ -232,7 +232,7 @@ namespace Barotrauma }; } - var reports = Order.PrefabList.FindAll(o => o.TargetAllCharacters && o.SymbolSprite != null); + var reports = Order.PrefabList.FindAll(o => o.IsReport && o.SymbolSprite != null); 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 +252,7 @@ namespace Barotrauma //report buttons foreach (Order order in reports) { - if (!order.TargetAllCharacters || order.SymbolSprite == null) { continue; } + if (!order.IsReport || order.SymbolSprite == null) { continue; } var btn = new GUIButton(new RectTransform(new Point(ReportButtonFrame.Rect.Width), ReportButtonFrame.RectTransform), style: null) { OnClicked = (GUIButton button, object userData) => @@ -667,6 +667,8 @@ 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 @@ -675,16 +677,69 @@ namespace Barotrauma { if (order != null && order.TargetAllCharacters) { - if (orderGiver == null || orderGiver.CurrentHull == null) { return; } - var hull = orderGiver.CurrentHull; - AddOrder(new Order(order.Prefab ?? order, hull, null, orderGiver), order.FadeOutTime); + Hull hull = null; + if (order.IsReport) + { + if (orderGiver?.CurrentHull == null) { return; } + hull = orderGiver.CurrentHull; + AddOrder(new Order(order.Prefab ?? order, hull, null, orderGiver), order.FadeOutTime); + } + else if(order.IsIgnoreOrder) + { + WallSection ws = null; + if (order.TargetType == Order.OrderTargetType.Entity && order.TargetEntity is MapEntity me) + { + 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); + } + } + else if (order.TargetType == Order.OrderTargetType.WallSection && order.TargetEntity is Structure s) + { + var wallSectionIndex = order.WallSectionIndex ?? s.Sections.IndexOf(wallContext); + 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); + } + } + } + + if (ws != null) + { + hull = Hull.FindHull(ws.WorldPosition); + } + else if (order.TargetEntity is Item i) + { + hull = i.CurrentHull; + } + else if (order.TargetEntity is ISpatialEntity se) + { + hull = Hull.FindHull(se.WorldPosition); + } + } + if (IsSinglePlayer) { - orderGiver.Speak(order.GetChatMessage("", hull.DisplayName, givingOrderToSelf: character == orderGiver), ChatMessageType.Order); + orderGiver.Speak(order.GetChatMessage("", hull?.DisplayName, givingOrderToSelf: character == orderGiver), ChatMessageType.Order); } else { - OrderChatMessage msg = new OrderChatMessage(order, "", hull, null, orderGiver); + OrderChatMessage msg = new OrderChatMessage(order, "", order.IsReport ? hull : order.TargetEntity, null, orderGiver); GameMain.Client?.SendChatMessage(msg); } } @@ -696,7 +751,7 @@ namespace Barotrauma if (IsSinglePlayer) { character.SetOrder(order, option, orderGiver, speak: orderGiver != character); - orderGiver?.Speak(order.GetChatMessage(character.Name, orderGiver.CurrentHull?.DisplayName, givingOrderToSelf: character == orderGiver, orderOption: option), null); + orderGiver?.Speak(order?.GetChatMessage(character.Name, orderGiver.CurrentHull?.DisplayName, givingOrderToSelf: character == orderGiver, orderOption: option)); } else if (orderGiver != null) { @@ -1591,6 +1646,7 @@ namespace Barotrauma private Character characterContext; private Item itemContext; private Hull hullContext; + private WallSection wallContext; private bool isContextual; private readonly List contextualOrders = new List(); private Point shorcutCenterNodeOffset; @@ -1640,7 +1696,7 @@ namespace Barotrauma } } - else if (TryGetBreachedHullAtHoveredWall(out Hull breachedHull)) + else if (TryGetBreachedHullAtHoveredWall(out Hull breachedHull, out wallContext)) { return breachedHull; } @@ -1662,16 +1718,24 @@ namespace Barotrauma if (entityContext is Character character) { characterContext = character; + itemContext = null; + hullContext = null; + wallContext = null; isContextual = false; } else if (entityContext is Item item) { itemContext = item; + characterContext = null; + hullContext = null; + wallContext = null; isContextual = true; } else if (entityContext is Hull hull) { hullContext = hull; + characterContext = null; + itemContext = null; isContextual = true; } @@ -1785,7 +1849,7 @@ namespace Barotrauma availableCategories = new List(); foreach (OrderCategory category in Enum.GetValues(typeof(OrderCategory))) { - if (Order.PrefabList.Any(o => o.Category == category && !o.TargetAllCharacters)) + if (Order.PrefabList.Any(o => o.Category == category && !o.IsReport)) { availableCategories.Add(category); } @@ -2129,7 +2193,7 @@ namespace Barotrauma if (Order.GetPrefab(orderIdentifier) is Order orderPrefab && shortcutNodes.None(n => (n.UserData is Order order && order.Identifier == orderIdentifier) || (n.UserData is Tuple orderWithOption && orderWithOption.Item1.Identifier == orderIdentifier)) && - !orderPrefab.TargetAllCharacters && orderPrefab.Category != null) + !orderPrefab.IsReport && orderPrefab.Category != null) { if (!orderPrefab.MustSetTarget || orderPrefab.GetMatchingItems(sub, true).Any()) { @@ -2172,7 +2236,7 @@ namespace Barotrauma private void CreateOrderNodes(OrderCategory orderCategory) { - var orders = Order.PrefabList.FindAll(o => o.Category == orderCategory && !o.TargetAllCharacters); + var orders = Order.PrefabList.FindAll(o => o.Category == orderCategory && !o.IsReport); Order order; bool disableNode; var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, nodeDistance, @@ -2250,34 +2314,38 @@ 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)) && AIObjectiveCleanupItems.IsValidTarget(itemContext, Character.Controlled, checkInventory: false)) { contextualOrders.Add(new Order(Order.GetPrefab(orderIdentifier), itemContext, targetItem: null, Character.Controlled)); } } + + AddIgnoreOrder(itemContext); } else if (hullContext != null) { contextualOrders.Add(new Order(Order.GetPrefab("fixleaks"), hullContext, targetItem: null, Character.Controlled)); + + if (wallContext != null) + { + AddIgnoreOrder(wallContext); + } } - if (contextualOrders.None(o => o.Category != OrderCategory.Movement)) + orderIdentifier = "wait"; + if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier))) { - orderIdentifier = "wait"; + Vector2 position = GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition); + Hull hull = Hull.FindHull(position, guess: Character.Controlled?.CurrentHull); + contextualOrders.Add(new Order(Order.GetPrefab(orderIdentifier), new OrderTarget(position, hull), Character.Controlled)); + } + + if (contextualOrders.None(o => o.Category != OrderCategory.Movement) && characters.Any(c => c != Character.Controlled)) + { + orderIdentifier = "follow"; if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier))) { - Vector2 position = GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition); - Hull hull = Hull.FindHull(position, guess: Character.Controlled?.CurrentHull); - contextualOrders.Add(new Order(Order.GetPrefab(orderIdentifier), new OrderTarget(position, hull), Character.Controlled)); - } - - if (characters.Any(c => c != Character.Controlled)) - { - orderIdentifier = "follow"; - if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier))) - { - contextualOrders.Add(Order.GetPrefab(orderIdentifier)); - } + contextualOrders.Add(Order.GetPrefab(orderIdentifier)); } } @@ -2298,6 +2366,35 @@ 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 @@ -2306,7 +2403,7 @@ 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 (item.Repairables.Any(r => item.ConditionPercentage < r.RepairThreshold)) { return true; } var operateWeaponsPrefab = Order.GetPrefab("operateweapons"); @@ -2367,11 +2464,13 @@ namespace Barotrauma { o = new Order(o.Prefab, orderTargetEntity, orderTargetEntity.Components.FirstOrDefault(ic => ic.GetType() == order.ItemComponentType), orderGiver: order.OrderGiver); } - SetCharacterOrder(characterContext ?? GetCharacterForQuickAssignment(o), o, null, Character.Controlled); + var character = !o.TargetAllCharacters ? characterContext ?? GetCharacterForQuickAssignment(o) : null; + SetCharacterOrder(character, o, null, 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 + "\n" + (!PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.leftmouse") : TextManager.Get("input.rightmouse")) + ": " + TextManager.Get("commandui.quickassigntooltip") + @@ -2948,9 +3047,10 @@ namespace Barotrauma return (degrees < 0) ? (degrees + 360) : degrees; } - private bool TryGetBreachedHullAtHoveredWall(out Hull breachedHull) + private bool TryGetBreachedHullAtHoveredWall(out Hull breachedHull, out WallSection hoveredWall) { breachedHull = null; + hoveredWall = null; // Based on the IsValidTarget() method of AIObjectiveFixLeaks class List leaks = Gap.GapList.FindAll(g => g != null && g.ConnectedWall != null && g.ConnectedDoor == null && g.Open > 0 && g.linkedTo.Any(l => l != null) && @@ -2962,6 +3062,15 @@ namespace Barotrauma if (Submarine.RectContains(leak.ConnectedWall.WorldRect, mouseWorldPosition)) { breachedHull = leak.FlowTargetHull; + foreach (var section in leak.ConnectedWall.Sections) + { + if (Submarine.RectContains(section.WorldRect, mouseWorldPosition)) + { + hoveredWall = section; + break; + } + + } return true; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs index ef2571f4b..c29550dcf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs @@ -20,6 +20,7 @@ namespace Barotrauma protected GUIButton endRoundButton; + public GUIButton ReadyCheckButton; public GUIButton EndRoundButton => endRoundButton; protected GUIFrame campaignUIContainer; @@ -142,7 +143,8 @@ namespace Barotrauma if (GUI.DisableHUD || GUI.DisableUpperHUD || ForceMapUI || CoroutineManager.IsCoroutineRunning("LevelTransition")) { - endRoundButton.Visible = false; + endRoundButton.Visible = false; + if (ReadyCheckButton != null) { ReadyCheckButton.Visible = false; } return; } if (Submarine.MainSub == null) { return; } @@ -188,6 +190,8 @@ namespace Barotrauma break; } + if (ReadyCheckButton != null) { ReadyCheckButton.Visible = endRoundButton.Visible; } + if (endRoundButton.Visible) { if (!AllowedToEndRound()) { buttonText = TextManager.Get("map"); } @@ -211,6 +215,10 @@ namespace Barotrauma } } endRoundButton.DrawManually(spriteBatch); + if (this is MultiPlayerCampaign) + { + ReadyCheckButton?.DrawManually(spriteBatch); + } } public Task SelectSummaryScreen(RoundSummary roundSummary, LevelData newLevel, bool mirror, Action action) @@ -279,6 +287,7 @@ namespace Barotrauma base.AddToGUIUpdateList(); CrewManager.AddToGUIUpdateList(); endRoundButton.AddToGUIUpdateList(); + ReadyCheckButton?.AddToGUIUpdateList(); } public override void Update(float deltaTime) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index 1ada97e3b..8abd2ea9f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -34,7 +34,6 @@ namespace Barotrauma } } - public static void StartCampaignSetup(IEnumerable saveFiles) { var parent = GameMain.NetLobbyScreen.CampaignSetupFrame; @@ -103,11 +102,13 @@ namespace Barotrauma { CanBeFocused = false }; - - int buttonHeight = (int)(GUI.Scale * 40); - int buttonWidth = GUI.IntScale(450); - endRoundButton = new GUIButton(HUDLayoutSettings.ToRectTransform(new Rectangle((GameMain.GraphicsWidth / 2) - (buttonWidth / 2), HUDLayoutSettings.ButtonAreaTop.Center.Y - (buttonHeight / 2), buttonWidth, buttonHeight), GUICanvas.Instance), + int buttonHeight = (int) (GUI.Scale * 40), + buttonWidth = GUI.IntScale(450), + buttonCenter = buttonHeight / 2, + screenMiddle = GameMain.GraphicsWidth / 2; + + endRoundButton = new GUIButton(HUDLayoutSettings.ToRectTransform(new Rectangle(screenMiddle - buttonWidth / 2, HUDLayoutSettings.ButtonAreaTop.Center.Y - buttonCenter, buttonWidth, buttonHeight), GUICanvas.Instance), TextManager.Get("EndRound"), textAlignment: Alignment.Center, style: "EndRoundButton") { Pulse = true, @@ -140,6 +141,25 @@ namespace Barotrauma return true; } }; + + int readyButtonHeight = buttonHeight; + int readyButtonWidth = (int) (GUI.Scale * 50); + + ReadyCheckButton = new GUIButton(HUDLayoutSettings.ToRectTransform(new Rectangle(screenMiddle + (buttonWidth / 2) + GUI.IntScale(16), HUDLayoutSettings.ButtonAreaTop.Center.Y - buttonCenter, readyButtonWidth, readyButtonHeight), GUICanvas.Instance), + style: "RepairBuyButton") + { + ToolTip = TextManager.Get("ReadyCheck.Tooltip"), + OnClicked = delegate + { + if (CrewManager != null && CrewManager.ActiveReadyCheck == null) + { + ReadyCheck.CreateReadyCheck(); + } + return true; + }, + UserData = "ReadyCheckButton" + }; + buttonContainer.Recalculate(); } @@ -378,6 +398,7 @@ namespace Barotrauma if (!GUI.DisableHUD && !GUI.DisableUpperHUD) { endRoundButton.UpdateManually(deltaTime); + ReadyCheckButton?.UpdateManually(deltaTime); if (CoroutineManager.IsCoroutineRunning("LevelTransition") || ForceMapUI) { return; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index e9f57df0c..33cc48da0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -368,6 +368,9 @@ 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; @@ -452,6 +455,8 @@ namespace Barotrauma } } + lvlData.IsBeaconActive = beaconActive; + SaveUtil.SaveGame(GameMain.GameSession.SavePath); } else 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/ScenarioTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs index 913fa9137..4de2994c4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs @@ -121,7 +121,7 @@ namespace Barotrauma.Tutorials return; } - character = Character.Create(charInfo, wayPoint.WorldPosition, "", false, false); + character = Character.Create(charInfo, wayPoint.WorldPosition, "", isRemotePlayer: false, hasAi: false); character.TeamID = Character.TeamType.Team1; Character.Controlled = character; character.GiveJobItems(null); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs new file mode 100644 index 000000000..cdb324954 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs @@ -0,0 +1,287 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + internal partial class ReadyCheck + { + private static string readyCheckBody(string name) => string.IsNullOrWhiteSpace(name) ? TextManager.Get("readycheck.serverbody") : TextManager.GetWithVariable("readycheck.body", "[player]", name); + + private static string readyCheckStatus(int ready, int total) => TextManager.GetWithVariables("readycheck.readycount", new[] { "[ready]", "[total]" }, new[] { ready.ToString(), total.ToString() }); + private static string readyCheckPleaseWait(int seconds) => TextManager.GetWithVariable("readycheck.pleasewait", "[seconds]", seconds.ToString()); + + private static readonly string readyCheckHeader = TextManager.Get("ReadyCheck.Title"); + + private static readonly string noButton = TextManager.Get("No"), + yesButton = TextManager.Get("Yes"), + closeButton = TextManager.Get("Close"); + + private const string TimerData = "Timer", + PromptData = "ReadyCheck", + ResultData = "ReadyCheckResults", + UserListData = "ReadyUserList", + ReadySpriteData = "ReadySprite"; + + private int lastSecond; + + private GUIMessageBox? msgBox; + private GUIMessageBox? resultsBox; + + public static DateTime lastReadyCheck = DateTime.MinValue; + + private void CreateMessageBox(string author) + { + Vector2 relativeSize = new Vector2(GUI.IsFourByThree() ? 0.3f : 0.2f, 0.15f); + Point minSize = new Point(300, 200); + msgBox = new GUIMessageBox(readyCheckHeader, readyCheckBody(author), new[] { yesButton, noButton }, relativeSize, minSize, type: GUIMessageBox.Type.Vote) { UserData = PromptData, Draggable = true }; + + GUILayoutGroup contentLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.125f), msgBox.Content.RectTransform), childAnchor: Anchor.Center); + new GUIProgressBar(new RectTransform(new Vector2(0.8f, 1f), contentLayout.RectTransform), time / endTime, GUI.Style.Orange) { UserData = TimerData }; + + // Yes + msgBox.Buttons[0].OnClicked = delegate + { + msgBox.Close(); + SendState(ReadyStatus.Yes); + CreateResultsMessage(); + return true; + }; + + // No + msgBox.Buttons[1].OnClicked = delegate + { + msgBox.Close(); + SendState(ReadyStatus.No); + CreateResultsMessage(); + return true; + }; + } + + private void CreateResultsMessage() + { + 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 }; + if (msgBox != null) + { + resultsBox.RectTransform.ScreenSpaceOffset = msgBox.RectTransform.ScreenSpaceOffset; + } + + GUIListBox listBox = new GUIListBox(new RectTransform(new Vector2(1f, 0.8f), resultsBox.Content.RectTransform)) { UserData = UserListData }; + + 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 }; + GUILayoutGroup frame = new GUILayoutGroup(new RectTransform(Vector2.One, container.RectTransform), isHorizontal: true) { Stretch = true }; + + int height = frame.Rect.Height; + + JobPrefab? jobPrefab = client?.Character?.Info?.Job?.Prefab; + + if (client == null) + { + string list = GameMain.Client.ConnectedClients.Aggregate("Available clients:\n", (current, c) => current + $"{c.ID}: {c.Name}\n"); + DebugConsole.ThrowError($"Client ID {id} was reported in ready check but was not found.\n" + list.TrimEnd('\n')); + } + + if (jobPrefab?.Icon != null) + { + // job icon + new GUIImage(new RectTransform(new Point(height, height), frame.RectTransform), jobPrefab.Icon, scaleToFit: true) { Color = jobPrefab.UIColor }; + } + + new GUITextBlock(new RectTransform(new Vector2(0.75f, 1), frame.RectTransform), client?.Name ?? $"Unknown ID {id}", jobPrefab?.UIColor ?? Color.White, textAlignment: Alignment.Center) { AutoScaleHorizontal = true }; + new GUIImage(new RectTransform(new Point(height, height), frame.RectTransform), null, scaleToFit: true) { UserData = ReadySpriteData }; + } + + resultsBox.Buttons[0].OnClicked = delegate + { + resultsBox.Close(); + return true; + }; + } + + private void UpdateBar() + { + if (msgBox != null && !msgBox.Closed && GUIMessageBox.MessageBoxes.Contains(msgBox)) + { + if (msgBox.FindChild(TimerData, true) is GUIProgressBar bar) + { + bar.BarSize = time / endTime; + } + } + + // play click sound after a second has passed + int second = (int) Math.Ceiling(time); + if (second < lastSecond) + { + SoundPlayer.PlayUISound(GUISoundType.PopupMenu); + lastSecond = second; + } + } + + public static void ClientRead(IReadMessage inc) + { + ReadyCheckState state = (ReadyCheckState) inc.ReadByte(); + CrewManager? crewManager = GameMain.GameSession?.CrewManager; + List otherClients = GameMain.Client.ConnectedClients; + 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(); + bool hasAuthor = inc.ReadBoolean(); + + if (hasAuthor) + { + authorId = inc.ReadByte(); + isOwn = authorId == GameMain.Client.ID; + } + + ushort clientCount = inc.ReadUInt16(); + List clients = new List(); + for (int i = 0; i < clientCount; i++) + { + clients.Add(inc.ReadByte()); + } + + ReadyCheck rCheck = new ReadyCheck(clients, duration); + crewManager.ActiveReadyCheck = rCheck; + + if (isOwn) + { + SendState(ReadyStatus.Yes); + rCheck.CreateResultsMessage(); + } + else + { + rCheck.CreateMessageBox(author); + } + + if (hasAuthor && rCheck.Clients.ContainsKey(authorId)) + { + rCheck.Clients[authorId] = ReadyStatus.Yes; + } + break; + case ReadyCheckState.Update: + float time = inc.ReadSingle(); + ReadyStatus newState = (ReadyStatus) inc.ReadByte(); + byte targetId = inc.ReadByte(); + if (crewManager.ActiveReadyCheck != null) + { + crewManager.ActiveReadyCheck.time = time; + crewManager.ActiveReadyCheck?.UpdateState(targetId, newState); + } + break; + case ReadyCheckState.End: + ushort count = inc.ReadUInt16(); + for (int i = 0; i < count; i++) + { + byte id = inc.ReadByte(); + ReadyStatus status = (ReadyStatus) inc.ReadByte(); + crewManager.ActiveReadyCheck?.UpdateState(id, status); + } + + crewManager.ActiveReadyCheck?.EndReadyCheck(); + crewManager.ActiveReadyCheck?.msgBox?.Close(); + crewManager.ActiveReadyCheck = null; + break; + } + } + + 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)); + } + + private void UpdateState(byte id, ReadyStatus status) + { + if (Clients.ContainsKey(id)) + { + Clients[id] = status; + } + + if (resultsBox == null || resultsBox.Closed || !GUIMessageBox.MessageBoxes.Contains(resultsBox)) { return; } + + if (resultsBox.Content.FindChild(UserListData) is GUIListBox userList) + { + // for some reason FindChild doesn't work here? + foreach (GUIComponent child in userList.Content.Children) + { + if (!(child.UserData is byte b) || b != id) { continue; } + + if (child.GetChild().FindChild(ReadySpriteData) is GUIImage image) + { + string style; + switch (status) + { + case ReadyStatus.Yes: + style = "MissionCompletedIcon"; + break; + case ReadyStatus.No: + style = "MissionFailedIcon"; + break; + default: + return; + } + + image.ApplyStyle(GUI.Style.GetComponentStyle(style)); + } + } + } + } + + private static void SendState(ReadyStatus status) + { + IWriteMessage msg = new WriteOnlyMessage(); + msg.Write((byte) ClientPacketHeader.READY_CHECK); + msg.Write((byte) ReadyCheckState.Update); + msg.Write((byte) status); + GameMain.Client?.ClientPeer?.Send(msg, DeliveryMethod.Reliable); + } + + public static void CreateReadyCheck() + { + if (lastReadyCheck < DateTime.Now) + { +#if !DEBUG + lastReadyCheck = DateTime.Now.AddMinutes(1); +#endif + IWriteMessage msg = new WriteOnlyMessage(); + msg.Write((byte) ClientPacketHeader.READY_CHECK); + msg.Write((byte) ReadyCheckState.Start); + GameMain.Client?.ClientPeer?.Send(msg, DeliveryMethod.Reliable); + return; + } + + GUIMessageBox msgBox = new GUIMessageBox(readyCheckHeader, readyCheckPleaseWait((lastReadyCheck - DateTime.Now).Seconds), new[] { closeButton }); + msgBox.Buttons[0].OnClicked = delegate + { + msgBox.Close(); + return true; + }; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs index 135e61b32..4a9530609 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs @@ -503,7 +503,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; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs index e3ddc5cf1..e2861f84d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs @@ -65,10 +65,8 @@ 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 if (Language == "French") { @@ -1335,7 +1333,7 @@ namespace Barotrauma }; msgBox.Buttons[0].OnClicked = (yesButton, obj) => { - LoadDefaultConfig(setLanguage: false); + LoadDefaultConfig(setLanguage: false, loadContentPackages: Screen.Selected != GameMain.GameScreen); CheckBindings(true); RefreshItemMessages(); ApplySettings(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs index 6443ffe97..1b8a90f60 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs @@ -291,6 +291,7 @@ namespace Barotrauma.Items.Components bool broken = msg.ReadBoolean(); bool forcedOpen = msg.ReadBoolean(); bool isStuck = msg.ReadBoolean(); + bool isJammed = msg.ReadBoolean(); SetState(open, isNetworkMessage: true, sendNetworkMessage: false, forcedOpen: forcedOpen); stuck = msg.ReadRangedSingle(0.0f, 100.0f, 8); UInt16 lastUserID = msg.ReadUInt16(); @@ -301,6 +302,7 @@ namespace Barotrauma.Items.Components toggleCooldownTimer = ToggleCoolDown; } this.isStuck = isStuck; + this.isJammed = isJammed; if (isStuck) { OpenState = 0.0f; } IsBroken = broken; PredictedState = null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Growable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Growable.cs index c83c16458..f5a2cda6e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Growable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Growable.cs @@ -8,46 +8,6 @@ using Microsoft.Xna.Framework.Graphics; namespace Barotrauma.Items.Components { - internal partial class VineTile - { - public void Draw(SpriteBatch spriteBatch, Vector2 position, float depth, float leafDepth) - { - Vector2 pos = position + Position; - pos.Y = -pos.Y; - - VineSprite vineSprite = Parent.VineSprites[Type]; - Color color = Parent.Decayed ? Parent.DeadTint : Parent.VineTint; - - float layer1 = depth + 0.01f, // flowers - layer2 = depth + 0.02f, // decay atlas - layer3 = depth + 0.03f; // branches and leaves - - float scale = Parent.VineScale * VineStep; - - if (Parent.VineAtlas != null) - { - spriteBatch.Draw(Parent.VineAtlas.Texture, pos + offset, vineSprite.SourceRect, color, 0f, vineSprite.AbsoluteOrigin, scale, SpriteEffects.None, layer3); - } - - if (Parent.DecayAtlas != null) - { - spriteBatch.Draw(Parent.DecayAtlas.Texture, pos, vineSprite.SourceRect, HealthColor, 0f, vineSprite.AbsoluteOrigin, scale, SpriteEffects.None, layer2); - } - - if (FlowerConfig.Variant >= 0 && !Parent.Decayed) - { - Sprite flowerSprite = Parent.FlowerSprites[FlowerConfig.Variant]; - flowerSprite.Draw(spriteBatch, pos, Parent.FlowerTint, flowerSprite.Origin, scale: Parent.BaseFlowerScale * FlowerConfig.Scale * FlowerStep, rotate: FlowerConfig.Rotation, depth: layer1); - } - - if (LeafConfig.Variant >= 0) - { - Sprite leafSprite = Parent.LeafSprites[LeafConfig.Variant]; - leafSprite.Draw(spriteBatch, pos, Parent.Decayed ? Parent.DeadTint : Parent.LeafTint, leafSprite.Origin, scale: Parent.BaseLeafScale * LeafConfig.Scale * FlowerStep, rotate: LeafConfig.Rotation, depth: layer3 + leafDepth); - } - } - } - internal class VineSprite { [Serialize("0,0,0,0", false)] @@ -97,7 +57,7 @@ namespace Barotrauma.Items.Components foreach (VineTile vine in Vines) { leafDepth += zStep; - vine.Draw(spriteBatch, planter.Item.DrawPosition + offset, depth, leafDepth); + DrawBranch(vine, spriteBatch, planter.Item.DrawPosition + offset, depth, leafDepth); } if (GameMain.DebugDraw) @@ -111,6 +71,43 @@ namespace Barotrauma.Items.Components } } } + + private void DrawBranch(VineTile vine, SpriteBatch spriteBatch, Vector2 position, float depth, float leafDepth) + { + Vector2 pos = position + vine.Position; + pos.Y = -pos.Y; + + VineSprite vineSprite = VineSprites[vine.Type]; + Color color = Decayed ? DeadTint : VineTint; + + float layer1 = depth + 0.01f, // flowers + layer2 = depth + 0.02f, // decay atlas + layer3 = depth + 0.03f; // branches and leaves + + float scale = VineScale * vine.VineStep; + + 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 && DecayAtlas.Loaded) + { + spriteBatch.Draw(DecayAtlas.Texture, pos, vineSprite.SourceRect, vine.HealthColor, 0f, vineSprite.AbsoluteOrigin, scale, SpriteEffects.None, layer2); + } + + if (vine.FlowerConfig.Variant >= 0 && !Decayed) + { + Sprite flowerSprite = FlowerSprites[vine.FlowerConfig.Variant]; + flowerSprite.Draw(spriteBatch, pos, FlowerTint, flowerSprite.Origin, scale: BaseFlowerScale * vine.FlowerConfig.Scale * vine.FlowerStep, rotate: vine.FlowerConfig.Rotation, depth: layer1); + } + + if (vine.LeafConfig.Variant >= 0) + { + Sprite leafSprite = LeafSprites[vine.LeafConfig.Variant]; + leafSprite.Draw(spriteBatch, pos, Decayed ? DeadTint : LeafTint, leafSprite.Origin, scale: BaseLeafScale * vine.LeafConfig.Scale * vine.FlowerStep, rotate: vine.LeafConfig.Rotation, depth: layer3 + leafDepth); + } + } partial void LoadVines(XElement element) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs index 14767d23e..7fb2f8093 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 { @@ -15,10 +12,16 @@ namespace Barotrauma.Items.Components get { return item.Rect.Size.ToVector2(); } } + + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) { - if (!IsActive || picker == null || !CanBeAttached(picker) || !picker.IsKeyDown(InputType.Aim) || picker != Character.Controlled) { 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( MathUtils.RoundTowardsClosest(picker.Position.X, Submarine.GridSize.X), diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs new file mode 100644 index 000000000..69bc06a85 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs @@ -0,0 +1,13 @@ +using Microsoft.Xna.Framework; +using System.Collections.Generic; + +namespace Barotrauma.Items.Components +{ + partial class IdCard + { + public Sprite StoredPortrait; + public Vector2 StoredSheetIndex; + public JobPrefab StoredJobPrefab; + public List StoredAttachments; + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Sprayer.cs index a1821a13f..7432e038a 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) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs index 205a72010..abef03494 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs @@ -257,6 +257,8 @@ namespace Barotrauma.Items.Components spriteEffects |= MathUtils.NearlyEqual(ItemRotation % 180, 90.0f) ? SpriteEffects.FlipHorizontally : SpriteEffects.FlipVertically; } + bool isWiringMode = SubEditorScreen.TransparentWiringMode && SubEditorScreen.IsWiringMode(); + int i = 0; foreach (Item containedItem in Inventory.Items) { @@ -278,7 +280,7 @@ namespace Barotrauma.Items.Components containedItem.Sprite.Draw( spriteBatch, new Vector2(currentItemPos.X, -currentItemPos.Y), - containedItem.GetSpriteColor(), + isWiringMode ? containedItem.GetSpriteColor() * 0.15f : containedItem.GetSpriteColor(), origin, -(containedItem.body == null ? 0.0f : containedItem.body.DrawRotation + MathHelper.ToRadians(-item.Rotation)), containedItem.Scale, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs index b565fb267..2f1cc9cad 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs @@ -95,23 +95,21 @@ 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 (item.linkedTo.Count(entity => entity is Item item && item.DisplaySideBySideWhenLinked) == 1) + if (GuiFrame != null && item.linkedTo.Count(entity => entity is Item item && item.DisplaySideBySideWhenLinked) == 1) { foreach (MapEntity linkedTo in item.linkedTo) { - if (!(linkedTo is Item linkedItem)) continue; - if (!linkedItem.Components.Any()) continue; + if (!(linkedTo is Item linkedItem) || !linkedItem.DisplaySideBySideWhenLinked) { continue; } + if (!linkedItem.Components.Any()) { continue; } - var itemContainer = linkedItem.Components.First(); - if (itemContainer == null) { continue; } - - if (!itemContainer.Item.DisplaySideBySideWhenLinked) continue; + var itemContainer = linkedItem.GetComponent(); + if (itemContainer?.GuiFrame == null || itemContainer.AllowUIOverlap) { continue; } // 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(GuiFrame.Rect.Width / -2 - padding, 0); - GuiFrame.RectTransform.AbsoluteOffset = new Point(itemContainer.GuiFrame.Rect.Width / 2 + padding, 0); + // 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); } } return base.Select(character); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index 8e036e5a0..7adcb9eb3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -212,23 +212,21 @@ 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 (item.linkedTo.Count(entity => entity is Item item && item.DisplaySideBySideWhenLinked) == 1) + if (GuiFrame != null && item.linkedTo.Count(entity => entity is Item item && item.DisplaySideBySideWhenLinked) == 1) { foreach (MapEntity linkedTo in item.linkedTo) { - if (!(linkedTo is Item linkedItem)) continue; - if (!linkedItem.Components.Any()) continue; - - var itemContainer = linkedItem.Components.First(); - if (itemContainer == null) { continue; } + if (!(linkedTo is Item linkedItem) || !linkedItem.DisplaySideBySideWhenLinked) { continue; } + if (!linkedItem.Components.Any()) { continue; } - if (!itemContainer.Item.DisplaySideBySideWhenLinked) continue; + var itemContainer = linkedItem.GetComponent(); + if (itemContainer?.GuiFrame == null || itemContainer.AllowUIOverlap) { continue; } // 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(GuiFrame.Rect.Width / -2 - padding, 0); - GuiFrame.RectTransform.AbsoluteOffset = new Point(itemContainer.GuiFrame.Rect.Width / 2 + padding, 0); + itemContainer.GuiFrame.RectTransform.AbsoluteOffset = new Point(-100, 0); + GuiFrame.RectTransform.AbsoluteOffset = new Point(100, 0); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs index 6e61f3319..e35c9a17b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs @@ -73,7 +73,7 @@ namespace Barotrauma.Items.Components { OnClicked = (button, data) => { - targetLevel = null; + TargetLevel = null; IsActive = !IsActive; if (GameMain.Client != null) { @@ -112,7 +112,7 @@ namespace Barotrauma.Items.Components { if (pumpSpeedLockTimer <= 0.0f) { - targetLevel = null; + TargetLevel = null; } float newValue = barScroll * 200.0f - 100.0f; if (Math.Abs(newValue - FlowPercentage) < 0.1f) { return false; } @@ -215,14 +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(); + 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 b59955c76..16fc3d2cc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -1,4 +1,5 @@ -using Barotrauma.Networking; +using Barotrauma.Extensions; +using Barotrauma.Networking; using FarseerPhysics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; @@ -14,7 +15,8 @@ namespace Barotrauma.Items.Components public enum BlipType { Default, - Disruption + Disruption, + Destructible } private PathFinder pathFinder; @@ -28,11 +30,18 @@ namespace Barotrauma.Items.Components private GUITickBox activeTickBox, passiveTickBox; private GUITextBlock signalWarningText; + private GUIFrame lowerAreaFrame; + private GUIScrollBar zoomSlider; private GUIButton directionalModeSwitch; private Vector2? pingDragDirection = null; + /// + /// Can be null if the property HasMineralScanner is false + /// + private GUIButton mineralScannerSwitch; + private GUIFrame controlContainer; private GUICustomComponent sonarView; @@ -46,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; @@ -60,10 +69,12 @@ namespace Barotrauma.Items.Components private const float DisruptionUpdateInterval = 0.2f; private float disruptionUpdateTimer; - private float zoomSqrt; - private float showDirectionalIndicatorTimer; + private List nearbyObjects = new List(); + private const float NearbyObjectUpdateInterval = 1.0f; + float nearbyObjectUpdateTimer; + //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>(); @@ -103,6 +114,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 } } }; @@ -114,6 +129,15 @@ namespace Barotrauma.Items.Components public static Vector2 GUISizeCalculation => Vector2.One * Math.Min(GUI.RelativeHorizontalAspectRatio, 1f) * sonarAreaSize; + private List>> MineralClusters { get; set; } + + private readonly List textBlocksToScaleAndNormalize = new List(); + + private bool isConnectedToSteering; + + private bool AllowUsingMineralScanner => + HasMineralScanner && !isConnectedToSteering; + partial void InitProjSpecific(XElement element) { System.Diagnostics.Debug.Assert(Enum.GetValues(typeof(BlipType)).Cast().All(t => blipColorGradient.ContainsKey(t))); @@ -151,7 +175,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; } } @@ -166,16 +192,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") { @@ -215,10 +245,15 @@ namespace Barotrauma.Items.Components passiveTickBox.TextBlock.OverrideTextColor(GUI.Style.TextColor); activeTickBox.TextBlock.OverrideTextColor(GUI.Style.TextColor); - var lowerArea = new GUIFrame(new RectTransform(new Vector2(1, 0.4f + extraHeight), paddedControlContainer.RectTransform, Anchor.BottomCenter), style: null); - var zoomContainer = new GUIFrame(new RectTransform(new Vector2(1, 0.45f), lowerArea.RectTransform, Anchor.TopCenter), style: null); + textBlocksToScaleAndNormalize.Clear(); + textBlocksToScaleAndNormalize.Add(passiveTickBox.TextBlock); + textBlocksToScaleAndNormalize.Add(activeTickBox.TextBlock); + + lowerAreaFrame = new GUIFrame(new RectTransform(new Vector2(1, 0.4f + extraHeight), paddedControlContainer.RectTransform, Anchor.BottomCenter), style: null); + var zoomContainer = new GUIFrame(new RectTransform(new Vector2(1, 0.45f), lowerAreaFrame.RectTransform, Anchor.TopCenter), style: null); var zoomText = new GUITextBlock(new RectTransform(new Vector2(0.3f, 0.6f), zoomContainer.RectTransform, Anchor.CenterLeft), TextManager.Get("SonarZoom"), font: GUI.SubHeadingFont, textAlignment: Alignment.CenterRight); + textBlocksToScaleAndNormalize.Add(zoomText); zoomSlider = new GUIScrollBar(new RectTransform(new Vector2(0.5f, 0.8f), zoomContainer.RectTransform, Anchor.CenterLeft) { RelativeOffset = new Vector2(0.35f, 0) @@ -236,9 +271,10 @@ 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), lowerArea.RectTransform, Anchor.BottomCenter), style: null); + 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") { OnClicked = (button, data) => @@ -255,11 +291,20 @@ namespace Barotrauma.Items.Components }; var directionalModeSwitchText = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1), directionalModeFrame.RectTransform, Anchor.CenterRight), TextManager.Get("SonarDirectionalPing"), GUI.Style.TextColor, GUI.SubHeadingFont, Alignment.CenterLeft); + textBlocksToScaleAndNormalize.Add(directionalModeSwitchText); + if (AllowUsingMineralScanner) + { + AddMineralScannerSwitchToGUI(); + } + else + { + mineralScannerSwitch = null; + } GuiFrame.CanBeFocused = false; - - GUITextBlock.AutoScaleAndNormalize(passiveTickBox.TextBlock, activeTickBox.TextBlock, zoomText, directionalModeSwitchText); + + GUITextBlock.AutoScaleAndNormalize(textBlocksToScaleAndNormalize); sonarView = new GUICustomComponent(new RectTransform(Vector2.One * 0.7f, GuiFrame.RectTransform, Anchor.BottomRight, scaleBasis: ScaleBasis.BothHeight), (spriteBatch, guiCustomComponent) => { DrawSonar(spriteBatch, guiCustomComponent.Rect); }, null); @@ -271,11 +316,16 @@ 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(passiveTickBox.TextBlock, activeTickBox.TextBlock, zoomText, directionalModeSwitchText); + GUITextBlock.AutoScaleAndNormalize(textBlocksToScaleAndNormalize); + } + else if (GUI.RelativeHorizontalAspectRatio > 0.75f) + { + sonarView.RectTransform.RelativeOffset = new Vector2(0.13f * GUI.RelativeHorizontalAspectRatio, 0); + sonarView.RectTransform.SetPosition(Anchor.BottomRight); } } @@ -293,10 +343,58 @@ namespace Barotrauma.Items.Components { base.OnItemLoaded(); zoomSlider.BarScroll = MathUtils.InverseLerp(MinZoom, MaxZoom, zoom); + 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 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.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) => + { + useMineralScanner = !useMineralScanner; + button.Selected = useMineralScanner; + if (GameMain.Client != null) + { + unsentChanges = true; + correctionTimer = CorrectionDelay; + } + return true; + } + }; + 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); + } + public override void UpdateHUD(Character character, float deltaTime, Camera cam) { showDirectionalIndicatorTimer -= deltaTime; @@ -339,6 +437,32 @@ namespace Barotrauma.Items.Components Vector2.DistanceSquared(sonarView.Rect.Center.ToVector2(), PlayerInput.MousePosition) < (sonarView.Rect.Width / 2 * sonarView.Rect.Width / 2); + if (AllowUsingMineralScanner && Level.Loaded != null && !Level.Loaded.Generating) + { + if (MineralClusters == null) + { + MineralClusters = new List>>(); + foreach (var p in Level.Loaded.PathPoints) + { + foreach (var c in p.ClusterLocations) + { + if (c.Resources.None(i => i != null && !i.Removed && i.Tags.Contains("ore"))) { continue; } + var pos = Vector2.Zero; + foreach (var r in c.Resources) + { + pos += r.WorldPosition; + } + pos /= c.Resources.Count; + MineralClusters.Add(new Tuple>(pos, c.Resources)); + } + } + } + else + { + MineralClusters.RemoveAll(t => t.Item2 == null || t.Item2.None() || t.Item2.All(i => i == null || i.Removed)); + } + } + if (UseTransducers && connectedTransducers.Count == 0) { return; @@ -348,21 +472,45 @@ 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]; - foreach (LevelObject levelObject in Level.Loaded.LevelObjectManager.GetAllObjects(transducerCenter, range * activePing.State / zoom)) + float pingRange = range * activePing.State / zoom; + 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) + { + ballastFloraSpores.Add(trigger); + } } } } @@ -400,6 +548,19 @@ namespace Barotrauma.Items.Components } } + foreach (LevelTrigger spore in ballastFloraSpores) + { + Vector2 blipPos = spore.WorldPosition + Rand.Vector(spore.ColliderRadius * Rand.Range(0.0f, 1.0f)); + SonarBlip sporeBlip = new SonarBlip(blipPos, Rand.Range(0.1f, 0.5f), 0.5f) + { + Rotation = Rand.Range(-MathHelper.TwoPi, MathHelper.TwoPi), + BlipType = BlipType.Default, + Velocity = Rand.Vector(100f, Rand.RandSync.Unsynced) + }; + + sonarBlips.Add(sporeBlip); + } + float outsideLevelFlow = 0.0f; if (transducerCenter.X < 0.0f) { @@ -721,6 +882,23 @@ namespace Barotrauma.Items.Components } } + if (AllowUsingMineralScanner && useMineralScanner && CurrentMode == Mode.Active && MineralClusters != null) + { + foreach (var t in MineralClusters) + { + var unobtainedMinerals = t.Item2.Where(i => i != null && i.GetRootInventoryOwner() == i); + if (unobtainedMinerals.None()) { continue; } + if (!CheckResourceMarkerVisibility(t.Item1, transducerCenter)) { continue; } + var i = unobtainedMinerals.FirstOrDefault(); + if (i == null) { continue; } + DrawMarker(spriteBatch, + i.Name, "mineral", i, + t.Item1, transducerCenter, + displayScale, center, DisplayRadius * 0.95f, + onlyShowTextOnMouseOver: true); + } + } + foreach (Submarine sub in Submarine.Loaded) { if (!sub.ShowSonarMarker) { continue; } @@ -967,8 +1145,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; } @@ -1101,9 +1278,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), @@ -1114,7 +1291,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); } } @@ -1203,7 +1381,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; @@ -1219,13 +1397,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(); @@ -1240,7 +1418,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); @@ -1252,8 +1430,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; @@ -1267,7 +1445,7 @@ namespace Barotrauma.Items.Components alpha -= 0.1f; } - if (alpha < 0) break; + if (alpha < 0) { break; } } } } @@ -1296,6 +1474,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); @@ -1334,7 +1536,8 @@ namespace Barotrauma.Items.Components sonarBlip.Draw(spriteBatch, center + pos, color * 0.5f, sonarBlip.Origin, 0, scale, SpriteEffects.None, 0); } - private void DrawMarker(SpriteBatch spriteBatch, string label, string iconIdentifier, object targetIdentifier, Vector2 worldPosition, Vector2 transducerPosition, float scale, Vector2 center, float radius) + private void DrawMarker(SpriteBatch spriteBatch, string label, string iconIdentifier, object targetIdentifier, Vector2 worldPosition, Vector2 transducerPosition, float scale, Vector2 center, float radius, + bool onlyShowTextOnMouseOver = false) { float linearDist = Vector2.Distance(worldPosition, transducerPosition); float dist = linearDist; @@ -1391,16 +1594,27 @@ namespace Barotrauma.Items.Components markerPos.Y = (int)markerPos.Y; float alpha = 1.0f; - if (linearDist * scale < radius) + if (!onlyShowTextOnMouseOver) { - float normalizedDist = linearDist * scale / radius; - alpha = Math.Max(normalizedDist - 0.4f, 0.0f); - - float mouseDist = Vector2.Distance(PlayerInput.MousePosition, markerPos); - float hoverThreshold = 150.0f; - if (mouseDist < hoverThreshold) + if (linearDist * scale < radius) { - alpha += (hoverThreshold - mouseDist) / hoverThreshold; + float normalizedDist = linearDist * scale / radius; + alpha = Math.Max(normalizedDist - 0.4f, 0.0f); + + float mouseDist = Vector2.Distance(PlayerInput.MousePosition, markerPos); + float hoverThreshold = 150.0f; + if (mouseDist < hoverThreshold) + { + alpha += (hoverThreshold - mouseDist) / hoverThreshold; + } + } + } + else + { + float mouseDist = Vector2.Distance(PlayerInput.MousePosition, markerPos); + if (mouseDist > 5) + { + alpha = 0.0f; } } @@ -1410,7 +1624,8 @@ namespace Barotrauma.Items.Components } else { - targetIcons[iconIdentifier].Draw(spriteBatch, markerPos); + var iconInfo = targetIcons[iconIdentifier]; + iconInfo.Item1.Draw(spriteBatch, markerPos, iconInfo.Item2); } if (alpha <= 0.0f) { return; } @@ -1441,11 +1656,13 @@ 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(); + + MineralClusters = null; } public void ClientWrite(IWriteMessage msg, object[] extraData = null) @@ -1460,6 +1677,7 @@ namespace Barotrauma.Items.Components float pingAngle = MathUtils.WrapAngleTwoPi(MathUtils.VectorToAngle(pingDirection)); msg.WriteRangedSingle(MathUtils.InverseLerp(0.0f, MathHelper.TwoPi, pingAngle), 0.0f, 1.0f, 8); } + msg.Write(useMineralScanner); } } @@ -1471,6 +1689,7 @@ namespace Barotrauma.Items.Components float zoomT = 1.0f; bool directionalPing = useDirectionalPing; float directionT = 0.0f; + bool mineralScanner = useMineralScanner; if (isActive) { zoomT = msg.ReadRangedSingle(0.0f, 1.0f, 8); @@ -1479,6 +1698,7 @@ namespace Barotrauma.Items.Components { directionT = msg.ReadRangedSingle(0.0f, 1.0f, 8); } + mineralScanner = msg.ReadBoolean(); } if (correctionTimer > 0.0f) @@ -1500,6 +1720,11 @@ namespace Barotrauma.Items.Components pingDirection = new Vector2((float)Math.Cos(pingAngle), (float)Math.Sin(pingAngle)); } useDirectionalPing = directionalModeSwitch.Selected = directionalPing; + useMineralScanner = mineralScanner; + if (mineralScannerSwitch != null) + { + mineralScannerSwitch.Selected = mineralScanner; + } } } @@ -1510,6 +1735,10 @@ namespace Barotrauma.Items.Components passiveTickBox.Selected = !isActive; activeTickBox.Selected = isActive; directionalModeSwitch.Selected = useDirectionalPing; + if (mineralScannerSwitch != null) + { + mineralScannerSwitch.Selected = useMineralScanner; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs index 559b44eac..fa0aba619 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs @@ -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"); @@ -319,8 +319,7 @@ namespace Barotrauma.Items.Components centerText = $"({TextManager.Get("Meter")})"; rightTextGetter = () => { - Vector2 pos = controlledSub == null ? Vector2.Zero : controlledSub.Position; - float realWorldDepth = Level.Loaded == null ? 0.0f : Math.Abs(pos.Y - Level.Loaded.Size.Y) * Physics.DisplayToRealWorldRatio; + float realWorldDepth = controlledSub == null ? -1000.0f : controlledSub.RealWorldDepth; return ((int)realWorldDepth).ToString(); }; break; @@ -340,9 +339,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"); @@ -437,7 +436,7 @@ 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; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs index 765a44282..afa0d6f39 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs @@ -1,8 +1,7 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; using System; -using System.Xml.Linq; +using System.Linq; namespace Barotrauma.Items.Components { @@ -10,6 +9,23 @@ namespace Barotrauma.Items.Components { public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { + bool launch = msg.ReadBoolean(); + if (launch) + { + ushort userId = msg.ReadUInt16(); + User = Entity.FindEntityByID(userId) as Character; + Vector2 simPosition = new Vector2(msg.ReadSingle(), msg.ReadSingle()); + float rotation = msg.ReadSingle(); + if (User != null) + { + Shoot(User, simPosition, simPosition, rotation, ignoredBodies: User.AnimController.Limbs.Where(l => !l.IsSevered).Select(l => l.body.FarseerBody).ToList(), createNetworkEvent: false); + } + else + { + Launch(User, simPosition, rotation); + } + } + bool isStuck = msg.ReadBoolean(); if (isStuck) { @@ -56,7 +72,15 @@ namespace Barotrauma.Items.Components else if (entity is Item item) { if (item.Removed) { return; } - StickToTarget(item.body.FarseerBody, axis); + var door = item.GetComponent(); + if (door != null) + { + StickToTarget(door.Body.FarseerBody, axis); + } + else if (item.body != null) + { + StickToTarget(item.body.FarseerBody, axis); + } } else if (entity is Submarine sub) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs index 0cfef7f33..d16f75c7f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs @@ -1,6 +1,5 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; using System.Linq; using System.Xml.Linq; @@ -70,7 +69,13 @@ namespace Barotrauma.Items.Components partial void ShowOnDisplay(string input) { - while (historyBox.Content.CountChildren > 60) + messageHistory.Add(input); + while (messageHistory.Count > MaxMessages) + { + messageHistory.RemoveAt(0); + } + + while (historyBox.Content.CountChildren > MaxMessages) { historyBox.RemoveChild(historyBox.Content.Children.First()); } @@ -114,11 +119,6 @@ namespace Barotrauma.Items.Components public override void AddToGUIUpdateList() { base.AddToGUIUpdateList(); - if (!string.IsNullOrEmpty(DisplayedWelcomeMessage)) - { - ShowOnDisplay(DisplayedWelcomeMessage); - DisplayedWelcomeMessage = ""; - } if (shouldSelectInputBox) { inputBox.Select(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs index 70fa59879..8cb27dcea 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 : Screen.Selected is SubEditorScreen editor && editor.WiringMode ? 0.00002f : 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) { @@ -261,7 +261,7 @@ namespace Barotrauma.Items.Components } else { - GUI.DrawRectangle(spriteBatch, drawPos + new Vector2(-3, -3), new Vector2(6, 6), item.Color, true, 0.0f); + GUI.DrawRectangle(spriteBatch, drawPos + new Vector2(-3, -3), new Vector2(6, 6), item.Color, true, 0.015f); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs index 4ce13c0a2..0f38b86d1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs @@ -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(); } @@ -286,75 +286,52 @@ namespace Barotrauma.Items.Components rotation + MathHelper.PiOver2, item.Scale, SpriteEffects.None, item.SpriteDepth + (barrelSprite.Depth - item.Sprite.Depth)); - if (!editing || GUI.DisableHUD || !item.IsSelected) { return; } + if (!GameMain.DebugDraw && (!editing || GUI.DisableHUD || !item.IsSelected)) { return; } - float widgetRadius = 60.0f; - - GUI.DrawLine(spriteBatch, - drawPos, - drawPos + new Vector2((float)Math.Cos(minRotation), (float)Math.Sin(minRotation)) * widgetRadius, - GUI.Style.Green); - - GUI.DrawLine(spriteBatch, - drawPos, - drawPos + new Vector2((float)Math.Cos(maxRotation), (float)Math.Sin(maxRotation)) * widgetRadius, - GUI.Style.Green); + 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, Color.LightGreen); - Widget minRotationWidget = GetWidget("minrotation", spriteBatch, size: 10, initMethod: (widget) => + if (GameMain.DebugDraw) { - widget.Selected += () => - { - oldRotation = RotationLimits; - }; - widget.MouseDown += () => - { - widget.color = GUI.Style.Green; - prevAngle = minRotation; - }; - widget.Deselected += () => - { - widget.color = Color.Yellow; - item.CreateEditingHUD(); - if (SubEditorScreen.IsSubEditor()) - { - SubEditorScreen.StoreCommand(new PropertyCommand(this, "RotationLimits", RotationLimits, oldRotation)); - } - }; - widget.MouseHeld += (deltaTime) => - { - minRotation = GetRotationAngle(GetDrawPos()); - if (minRotation > maxRotation) - { - float temp = minRotation; - minRotation = maxRotation; - maxRotation = temp; - } - MapEntity.DisableSelect = true; - }; - widget.PreUpdate += (deltaTime) => - { - widget.DrawPos = new Vector2(widget.DrawPos.X, -widget.DrawPos.Y); - widget.DrawPos = Screen.Selected.Cam.WorldToScreen(widget.DrawPos); - }; - widget.PostUpdate += (deltaTime) => - { - widget.DrawPos = Screen.Selected.Cam.ScreenToWorld(widget.DrawPos); - widget.DrawPos = new Vector2(widget.DrawPos.X, -widget.DrawPos.Y); - }; - widget.PreDraw += (sprtBtch, deltaTime) => - { - widget.tooltip = "Min: " + (int)MathHelper.ToDegrees(minRotation); - widget.DrawPos = GetDrawPos() + new Vector2((float)Math.Cos(minRotation), (float)Math.Sin(minRotation)) * widgetRadius; - widget.Update(deltaTime); - }; - }); - - Widget maxRotationWidget = GetWidget("maxrotation", spriteBatch, size: 10, initMethod: (widget) => + center = new Vector2((float)Math.Cos(targetRotation), (float)Math.Sin(targetRotation)); + GUI.DrawLine(spriteBatch, + drawPos, + drawPos + center * widgetRadius, + Color.Red); + + for (int i = 0; i < 5; i++) + { + center = new Vector2((float)Math.Cos(rotation + (angularVelocity * 0.05f * i)), (float)Math.Sin(rotation + (angularVelocity * 0.05f * i))); + GUI.DrawLine(spriteBatch, + drawPos, + drawPos + center * widgetRadius, + Color.Lerp(Color.Black, Color.Yellow, i * 0.25f)); + } + } + + const float coneRadius = 300.0f; + float radians = maxRotation - minRotation; + float circleRadius = coneRadius / Screen.Selected.Cam.Zoom * GUI.Scale; + float lineThickness = 1f / Screen.Selected.Cam.Zoom; + + if (radians > Math.PI * 2) + { + spriteBatch.DrawCircle(drawPos, circleRadius, 180, GUI.Style.Red, thickness: lineThickness); + } + else + { + spriteBatch.DrawSector(drawPos, circleRadius, radians, (int)Math.Abs(90 * radians), GUI.Style.Green, offset: minRotation, thickness: lineThickness); + } + + 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 += () => { @@ -369,6 +346,52 @@ namespace Barotrauma.Items.Components { widget.color = Color.Yellow; item.CreateEditingHUD(); + RotationLimits = RotationLimits; + if (SubEditorScreen.IsSubEditor()) + { + SubEditorScreen.StoreCommand(new PropertyCommand(this, "RotationLimits", RotationLimits, oldRotation)); + } + }; + widget.MouseHeld += (deltaTime) => + { + minRotation = GetRotationAngle(GetDrawPos()); + UpdateBarrel(); + MapEntity.DisableSelect = true; + }; + widget.PreUpdate += (deltaTime) => + { + widget.DrawPos = new Vector2(widget.DrawPos.X, -widget.DrawPos.Y); + widget.DrawPos = Screen.Selected.Cam.WorldToScreen(widget.DrawPos); + }; + widget.PostUpdate += (deltaTime) => + { + widget.DrawPos = Screen.Selected.Cam.ScreenToWorld(widget.DrawPos); + widget.DrawPos = new Vector2(widget.DrawPos.X, -widget.DrawPos.Y); + }; + widget.PreDraw += (sprtBtch, deltaTime) => + { + widget.tooltip = "Min: " + (int)MathHelper.ToDegrees(minRotation); + widget.DrawPos = GetDrawPos() + new Vector2((float)Math.Cos(minRotation), (float)Math.Sin(minRotation)) * coneRadius / Screen.Selected.Cam.Zoom * GUI.Scale; + widget.Update(deltaTime); + }; + }); + + Widget maxRotationWidget = GetWidget("maxrotation", spriteBatch, size: widgetSize, thickness: widgetThickness, initMethod: (widget) => + { + widget.Selected += () => + { + oldRotation = RotationLimits; + }; + widget.MouseDown += () => + { + widget.color = GUI.Style.Green; + prevAngle = maxRotation; + }; + widget.Deselected += () => + { + widget.color = Color.Yellow; + item.CreateEditingHUD(); + RotationLimits = RotationLimits; if (SubEditorScreen.IsSubEditor()) { SubEditorScreen.StoreCommand(new PropertyCommand(this, "RotationLimits", RotationLimits, oldRotation)); @@ -377,12 +400,7 @@ namespace Barotrauma.Items.Components widget.MouseHeld += (deltaTime) => { maxRotation = GetRotationAngle(GetDrawPos()); - if (minRotation > maxRotation) - { - float temp = minRotation; - minRotation = maxRotation; - maxRotation = temp; - } + UpdateBarrel(); MapEntity.DisableSelect = true; }; widget.PreUpdate += (deltaTime) => @@ -398,7 +416,7 @@ namespace Barotrauma.Items.Components widget.PreDraw += (sprtBtch, deltaTime) => { widget.tooltip = "Max: " + (int)MathHelper.ToDegrees(maxRotation); - widget.DrawPos = GetDrawPos() + new Vector2((float)Math.Cos(maxRotation), (float)Math.Sin(maxRotation)) * widgetRadius; + widget.DrawPos = GetDrawPos() + new Vector2((float)Math.Cos(maxRotation), (float)Math.Sin(maxRotation)) * coneRadius / Screen.Selected.Cam.Zoom * GUI.Scale; widget.Update(deltaTime); }; }); @@ -412,22 +430,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; } @@ -580,7 +608,6 @@ namespace Barotrauma.Items.Components } Launch(projectile, launchRotation: newTargetRotation); } - } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index 3ba47d040..13dd24e00 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -439,7 +439,7 @@ namespace Barotrauma protected virtual void ControlInput(Camera cam) { // Note that these targets are static. Therefore the outcome is the same if this method is called multiple times or only once. - if (selectedSlot != null && !DraggingItemToWorld) + if (selectedSlot != null && !DraggingItemToWorld && cam.GetZoomAmountFromPrevious() <= 0.25f) { cam.Freeze = true; } @@ -461,7 +461,7 @@ namespace Barotrauma }*/ bool mouseOn = interactRect.Contains(PlayerInput.MousePosition) && !Locked && !mouseOnGUI && !slot.Disabled; - + // Delete item from container in sub editor if (SubEditorScreen.IsSubEditor() && PlayerInput.IsCtrlDown()) { @@ -477,8 +477,12 @@ namespace Barotrauma SoundPlayer.PlayUISound(GUISoundType.PickItem); } - SubEditorScreen.BulkItemBufferInUse = true; - SubEditorScreen.BulkItemBuffer.Add(new AddOrDeleteCommand(new List { item }, true)); + if (!item.Removed) + { + SubEditorScreen.BulkItemBufferInUse = SubEditorScreen.ItemRemoveMutex; + SubEditorScreen.BulkItemBuffer.Add(new AddOrDeleteCommand(new List { item }, true)); + } + item.OwnInventory?.DeleteAllItems(); item.Remove(); } @@ -688,6 +692,18 @@ namespace Barotrauma subInventory.Update(deltaTime, cam, true); } + public void ClearSubInventories() + { + if (highlightedSubInventorySlots.Count == 0) return; + + foreach (SlotReference highlightedSubInventorySlot in highlightedSubInventorySlots) + { + highlightedSubInventorySlot.Inventory.HideTimer = 0.0f; + } + + highlightedSubInventorySlots.Clear(); + } + public virtual void Draw(SpriteBatch spriteBatch, bool subInventory = false) { if (slots == null || isSubInventory != subInventory) return; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index dbca8e7a8..46826b42e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -62,9 +62,9 @@ namespace Barotrauma } } - public override bool DrawBelowWater => (!(Screen.Selected is SubEditorScreen editor) || !editor.WiringMode || !isWire) && (base.DrawBelowWater || ParentInventory is CharacterInventory); + public override bool DrawBelowWater => (!(Screen.Selected is SubEditorScreen editor) || !editor.WiringMode || !isWire || !isLogic) && (base.DrawBelowWater || ParentInventory is CharacterInventory); - public override bool DrawOverWater => base.DrawOverWater || (IsSelected || Screen.Selected is SubEditorScreen editor && editor.WiringMode) && isWire; + public override bool DrawOverWater => base.DrawOverWater || (IsSelected || Screen.Selected is SubEditorScreen editor && editor.WiringMode) && (isWire || isLogic); private GUITextBlock itemInUseWarning; private GUITextBlock ItemInUseWarning @@ -93,6 +93,11 @@ namespace Barotrauma } } + public float GetDrawDepth() + { + return GetDrawDepth(SpriteDepth, Sprite); + } + public Color GetSpriteColor() { Color color = spriteColor; @@ -239,6 +244,10 @@ namespace Barotrauma 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.TransparentWiringMode && SubEditorScreen.IsWiringMode() && !isWire && parentInventory == null; + bool renderTransparent = isWiringMode && GetComponent() == null; + if (renderTransparent) { color *= 0.15f; } BrokenItemSprite fadeInBrokenSprite = null; float fadeInBrokenSpriteAlpha = 0.0f; @@ -269,6 +278,7 @@ namespace Barotrauma } float depth = GetDrawDepth(); + if (isWiringMode && isLogic && !PlayerInput.IsShiftDown()) { depth = 0.01f; } if (activeSprite != null) { SpriteEffects oldEffects = activeSprite.effects; @@ -296,6 +306,8 @@ namespace Barotrauma { if (!spriteAnimState[decorativeSprite].IsActive) { continue; } Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, -rotationRad) * Scale; + if (flippedX && Prefab.CanSpriteFlipX) { offset.X = -offset.X; } + if (flippedY && Prefab.CanSpriteFlipY) { offset.Y = -offset.Y; } decorativeSprite.Sprite.DrawTiled(spriteBatch, new Vector2(DrawPosition.X + offset.X - rect.Width / 2, -(DrawPosition.Y + offset.Y + rect.Height / 2)), size, color: color, @@ -316,11 +328,18 @@ 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) + { + 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); + } 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; + 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, depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth), 0.999f)); @@ -367,7 +386,8 @@ namespace Barotrauma if (!spriteAnimState[decorativeSprite].IsActive) { continue; } float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState); Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, -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); var sa = (float)Math.Sin(-body.Rotation); Vector2 transformedOffset = new Vector2(ca * offset.X + sa * offset.Y, -sa * offset.X + ca * offset.Y); @@ -386,9 +406,10 @@ namespace Barotrauma { if (!spriteAnimState[decorativeSprite].IsActive) { continue; } float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState); - var (xOff, yOff) = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, -rotationRad) * Scale; - - decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + xOff, -(DrawPosition.Y + yOff)), color, + Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, -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, depth: depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth)); } @@ -417,6 +438,14 @@ namespace Barotrauma } } + if (editing && IsSelected && PlayerInput.KeyDown(Keys.Space)) + { + if (GetComponent() is { } discharger) + { + discharger.DrawElectricity(spriteBatch); + } + } + if (!editing || (body != null && !body.Enabled)) { return; @@ -505,7 +534,19 @@ namespace Barotrauma } if (Screen.Selected != GameMain.SubEditorScreen) { return; } - + + if (GetComponent() is { } discharger) + { + if (PlayerInput.KeyDown(Keys.Space)) + { + discharger.FindNodes(WorldPosition, discharger.Range); + } + else + { + discharger.IsActive = false; + } + } + if (Character.Controlled == null) { activeHUDs.Clear(); } if (!Linkable) { return; } @@ -1341,6 +1382,10 @@ namespace Barotrauma DebugConsole.Log($"Received entity spawn message for item \"{itemName}\" (identifier: {itemIdentifier}, id: {itemId})"); + var itemPrefab = string.IsNullOrEmpty(itemIdentifier) ? + MapEntityPrefab.Find(itemName, null, showErrorMessages: false) as ItemPrefab : + MapEntityPrefab.Find(itemName, itemIdentifier, showErrorMessages: false) as ItemPrefab; + Vector2 pos = Vector2.Zero; Submarine sub = null; int itemContainerIndex = -1; @@ -1369,16 +1414,24 @@ namespace Barotrauma string tags = ""; if (tagsChanged) { - tags = msg.ReadString(); + string[] addedTags = msg.ReadString().Split(','); + string[] removedTags = msg.ReadString().Split(','); + if (itemPrefab != null) + { + tags = string.Join(',',itemPrefab.Tags.Where(t => !removedTags.Contains(t)).Concat(addedTags)); + } + } + bool isNameTag = msg.ReadBoolean(); + string writtenName = ""; + if (isNameTag) + { + writtenName = msg.ReadString(); } - if (!spawn) return null; + if (!spawn) { return null; } //---------------------------------------- - var itemPrefab = string.IsNullOrEmpty(itemIdentifier) ? - MapEntityPrefab.Find(itemName, null, showErrorMessages: false) as ItemPrefab : - MapEntityPrefab.Find(itemName, itemIdentifier, showErrorMessages: false) as ItemPrefab; if (itemPrefab == null) { string errorMsg = "Failed to spawn item, prefab not found (name: " + (itemName ?? "null") + ", identifier: " + (itemIdentifier ?? "null") + ")"; @@ -1425,9 +1478,8 @@ namespace Barotrauma } } - var item = new Item(itemPrefab, pos, sub) + var item = new Item(itemPrefab, pos, sub, id: itemId) { - ID = itemId, SpawnedInOutpost = spawnedInOutpost }; @@ -1442,6 +1494,11 @@ namespace Barotrauma } if (descriptionChanged) { item.Description = itemDesc; } if (tagsChanged) { item.Tags = tags; } + var nameTag = item.GetComponent(); + if (nameTag != null) + { + nameTag.WrittenName = writtenName; + } if (sub != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs index 4e0f2271d..b8896c2ae 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs @@ -56,6 +56,8 @@ namespace Barotrauma public Dictionary> DecorativeSpriteGroups = new Dictionary>(); public Sprite InventoryIcon; public Sprite MinimapIcon; + public Sprite InfectedSprite; + public Sprite DamagedInfectedSprite; //only used to display correct color in the sub editor, item instances have their own property that can be edited on a per-item basis [Serialize("1.0,1.0,1.0,1.0", false)] diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs new file mode 100644 index 000000000..59e59c823 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs @@ -0,0 +1,395 @@ +#nullable enable +using System; +using System.Collections.Generic; +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; +using Microsoft.Xna.Framework.Graphics; + +namespace Barotrauma.MapCreatures.Behavior +{ + partial class BallastFloraBehavior + { + + // ReSharper disable AutoPropertyCanBeMadeGetOnly.Global, UnusedAutoPropertyAccessor.Global, MemberCanBePrivate.Global + internal class DamageParticle + { + [Serialize(defaultValue: "", isSaveable: false)] + public string Identifier { get; set; } = ""; + + [Serialize(defaultValue: 0f, isSaveable: false)] + public float MinRotation { get; set; } + + [Serialize(defaultValue: 0f, isSaveable: false)] + public float MaxRotation { get; set; } + + [Serialize(defaultValue: 0f, isSaveable: false)] + public float MinVelocity { get; set; } + + [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) + { + Particle particle = GameMain.ParticleManager.CreateParticle(Identifier, pos, RandRotation(), RandVelocity()); + if (particle != null) + { + particle.ColorMultiplier = ColorMultiplier.ToVector4(); + } + } + + public DamageParticle(XElement element) + { + SerializableProperty.DeserializeProperties(this, element); + } + } + + public Sprite? branchAtlas, decayAtlas; + public readonly Dictionary BranchSprites = new Dictionary(); + public readonly List FlowerSprites = new List(), DamagedFlowerSprites = new List(); + public readonly List HiddenFlowerSprites = new List(); + public readonly List LeafSprites = new List(), DamagedLeafSprites = new List(); + + public readonly List DamageParticles = new List(); + public readonly List DeathParticles = new List(); + + partial void LoadPrefab(XElement element) + { + string? branchAtlasPath = element.GetAttributeString("branchatlas", null); + string? decayAtlasPath = element.GetAttributeString("decayatlas", null); + + if (branchAtlasPath != null) + { + branchAtlas = new Sprite(branchAtlasPath, Rectangle.Empty); + } + + if (decayAtlasPath != null) + { + decayAtlas = new Sprite(decayAtlasPath, Rectangle.Empty); + } + + foreach (XElement subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "branchsprite": + var tileType = subElement.GetAttributeString("type", null); + VineTileType type = Enum.Parse(tileType); + BranchSprites.Add(type, new VineSprite(subElement)); + break; + case "flowersprite": + FlowerSprites.Add(new Sprite(subElement)); + break; + case "damagedflowersprite": + DamagedFlowerSprites.Add(new Sprite(subElement)); + break; + case "hiddenflowersprite": + HiddenFlowerSprites.Add(new Sprite(subElement)); + break; + case "leafsprite": + LeafSprites.Add(new Sprite(subElement)); + break; + case "damagedleafsprite": + DamagedLeafSprites.Add(new Sprite(subElement)); + break; + case "damageparticle": + DamageParticles.Add(new DamageParticle(subElement)); + break; + case "deathparticle": + DeathParticles.Add(new DamageParticle(subElement)); + break; + case "targets": + LoadTargets(subElement); + break; + } + + flowerVariants = FlowerSprites.Count; + leafVariants = LeafSprites.Count; + } + } + + private void CreateShapnel(Vector2 pos) + { + 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))); + } + } + + private void CreateDamageParticle(BallastFloraBranch branch, float damage) + { + Vector2 pos = GetWorldPosition() + branch.Position; + int amount = (int)Math.Clamp(damage / 10f, 1, 10); + for (int i = 0; i < amount; i++) + { + foreach (DamageParticle particle in DamageParticles) + { + particle.Emit(pos); + } + } + } + + 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.000001f; + float leafDepth = zStep; + float flowerDepth = zStep; + + if (GameMain.DebugDraw) + { + foreach (Body body in bodies) + { + Vector2 pos = Parent.Submarine.DrawPosition + ConvertUnits.ToDisplayUnits(body.Position); + pos.Y = -pos.Y; + GUI.DrawRectangle(spriteBatch, pos, 32f, 32f, 0f, Color.Cyan, 0.1f, thickness: 1); + } + + foreach (var (key, steps) in IgnoredTargets) + { + string label = $"Ignored \"{key.Name}\" for {steps} steps"; + var (sizeX, sizeY) = GUI.SubHeadingFont.MeasureString(label); + Vector2 targetPos = key.WorldPosition; + targetPos.Y = -targetPos.Y; + GUI.DrawString(spriteBatch, targetPos - new Vector2(sizeX / 2f, sizeY), label, GUI.Style.Red, font: GUI.SubHeadingFont); + } + } + + foreach (BallastFloraBranch branch in Branches) + { + Vector2 pos = Parent.DrawPosition + Offset + branch.Position; + pos.Y = -pos.Y; + + float depth = BranchDepth; + + float layer1 = depth + 0.01f, + layer2 = depth + 0.02f, + layer3 = depth + 0.03f; + + VineSprite branchSprite = BranchSprites[branch.Type]; + + Color branchColor = Color.White; + + if (GameMain.DebugDraw) + { +#if DEBUG + Vector2 basePos = Parent.WorldPosition; + foreach (var (from, to) in debugSearchLines) + { + Vector2 pos1 = basePos - from; + pos1.Y = -pos1.Y; + Vector2 pos2 = basePos - to; + pos2.Y = -pos2.Y; + GUI.DrawLine(spriteBatch, pos1, pos2, GUI.Style.Yellow * 0.8f, width: 4); + } +#endif + + string label = ""; + + if (branch == Branches[^1]) + { + label += $"Current State: {StateMachine.State?.GetType().Name ?? "null!"}\n"; + } + + if (StateMachine.State is GrowToTargetState targetState) + { + if (targetState.TargetBranches.Contains(branch)) + { + GUI.DrawRectangle(spriteBatch, pos, branch.Rect.Width, branch.Rect.Height, 0f, Color.Red, thickness: 4); + } + + if (targetState.TargetBranches[^1] == branch) + { + label += $"Target: {targetState.Target.Name}\n"; + + Vector2 targetPos = targetState.Target.WorldPosition; + targetPos.Y = -targetPos.Y; + GUI.DrawLine(spriteBatch, pos, targetPos, Color.Red, width: 4); + } + } + + var (sizeX, sizeY) = GUI.SubHeadingFont.MeasureString(label); + GUI.DrawString(spriteBatch, pos - new Vector2(sizeX / 2f, branch.Rect.Height + sizeY), label, Color.White, font: GUI.SubHeadingFont); + } + + bool isDamaged = branch.Health < branch.MaxHealth; + + if (HasBrokenThrough) + { + 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 && decayAtlas.Loaded) + { + spriteBatch.Draw(decayAtlas.Texture, pos + branch.offset, branchSprite.SourceRect, branch.HealthColor, 0f, branchSprite.AbsoluteOrigin, BaseBranchScale * branch.VineStep, SpriteEffects.None, layer2 - zStep); + } + } + + if (branch.FlowerConfig.Variant >= 0) + { + int variant = branch.FlowerConfig.Variant; + Sprite flowerSprite = HasBrokenThrough ? FlowerSprites[variant] : HiddenFlowerSprites[variant]; + float flowerScale = BaseFlowerScale * branch.FlowerConfig.Scale * branch.FlowerStep; + + if (HasBrokenThrough) { flowerScale *= branch.Pulse; } + + flowerSprite.Draw(spriteBatch, pos, branchColor, flowerSprite.Origin, scale: flowerScale, rotate: branch.FlowerConfig.Rotation, depth: layer1 - flowerDepth); + if (isDamaged && HasBrokenThrough && DamagedFlowerSprites.Count > variant) + { + 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) + { + int variant = branch.LeafConfig.Variant; + Sprite leafSprite = LeafSprites[variant]; + leafSprite.Draw(spriteBatch, pos, branchColor, leafSprite.Origin, scale: BaseLeafScale * branch.LeafConfig.Scale * branch.FlowerStep, rotate: branch.LeafConfig.Rotation, depth: layer3 + leafDepth); + if (isDamaged && DamagedLeafSprites.Count > variant) + { + 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; + } + } + } + } + + + public void ClientRead(IReadMessage msg, NetworkHeader header) + { + switch (header) + { + case NetworkHeader.Infect: + int infectBranch = -1; + ushort itemId = msg.ReadUInt16(); + bool infect = msg.ReadBoolean(); + if (infect) + { + infectBranch = msg.ReadInt32(); + } + + Entity? entity = Entity.FindEntityByID(itemId); + if (entity is Item item) + { + if (infect) + { + 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); + + 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; + case NetworkHeader.BranchRemove: + + int removedBranchId = msg.ReadInt32(); + BallastFloraBranch removedBranch = Branches.FirstOrDefault(b => b.ID == removedBranchId); + 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: + + int damageBranchId = msg.ReadInt32(); + float damage = msg.ReadSingle(); + float health = msg.ReadSingle(); + BallastFloraBranch damagedBranch = Branches.FirstOrDefault(b => b.ID == damageBranchId); + if (damagedBranch != null) + { + 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(); + break; + } + + PowerConsumptionTimer = msg.ReadSingle(); + } + + private BallastFloraBranch ReadBranch(IReadMessage msg) + { + int id = msg.ReadInt32(); + byte type = (byte) msg.ReadRangedInteger(0b0000, 0b1111); + byte sides = (byte) msg.ReadRangedInteger(0b0000, 0b1111); + int flowerConfig = msg.ReadRangedInteger(0, 0xFFF); + int leafConfig = msg.ReadRangedInteger(0, 0xFFF); + int maxHealth = msg.ReadUInt16(); + int posX = msg.ReadInt32(), posY = msg.ReadInt32(); + Vector2 pos = new Vector2(posX * VineTile.Size, posY * VineTile.Size); + + return new BallastFloraBranch(this, pos, (VineTileType)type, FoliageConfig.Deserialize(flowerConfig), FoliageConfig.Deserialize(leafConfig)) + { + ID = id, + MaxHealth = maxHealth, + Sides = (TileSide) sides + }; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/FireSource.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/FireSource.cs index 694e71f73..a131cd4a9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/FireSource.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/FireSource.cs @@ -36,8 +36,8 @@ namespace Barotrauma Rand.Range(worldPosition.Y - size.Y, worldPosition.Y + 20.0f)); Vector2 particleVel = new Vector2( - (particlePos.X - (worldPosition.X + size.X / 2.0f)), - (float)Math.Sqrt(size.X) * Rand.Range(0.0f, 15.0f) * growModifier); + particlePos.X - (worldPosition.X + size.X / 2.0f), + Math.Max((float)Math.Sqrt(size.X) * Rand.Range(0.0f, 15.0f) * growModifier, 0.0f)); particleVel.X = MathHelper.Clamp(particleVel.X, -200.0f, 200.0f); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs index a99f8e36f..82627664c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs @@ -5,6 +5,7 @@ using Microsoft.Xna.Framework.Input; using System; using System.Collections.Generic; using System.Linq; +using Barotrauma.MapCreatures.Behavior; namespace Barotrauma { @@ -48,7 +49,7 @@ namespace Barotrauma { get { - return decals.Count > 0; + return decals.Count > 0 || BallastFlora != null; } } @@ -65,6 +66,8 @@ namespace Barotrauma public override bool IsVisible(Rectangle worldView) { + if (BallastFlora != null) { return true; } + if (Screen.Selected != GameMain.SubEditorScreen && !GameMain.DebugDraw) { if (decals.Count == 0 && paintAmount < minimumPaintAmountToDraw) { return false; } @@ -229,6 +232,7 @@ namespace Barotrauma { if (back && Screen.Selected != GameMain.SubEditorScreen) { + BallastFlora?.Draw(spriteBatch); DrawDecals(spriteBatch); return; } @@ -244,7 +248,7 @@ namespace Barotrauma alpha = Math.Min((float)(Timing.TotalTime - lastAmbientLightEditTime) / hideTimeAfterEdit - 1.0f, 1.0f); } - Rectangle drawRect = + Rectangle drawRect = Submarine == null ? rect : new Rectangle((int)(Submarine.DrawPosition.X + rect.X), (int)(Submarine.DrawPosition.Y + rect.Y), rect.Width, rect.Height); if ((IsSelected || IsHighlighted) && editing) @@ -332,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); @@ -607,6 +612,26 @@ namespace Barotrauma public void ClientRead(ServerNetObject type, IReadMessage message, float sendingTime) { + bool isBallastFloraUpdate = message.ReadBoolean(); + if (isBallastFloraUpdate) + { + BallastFloraBehavior.NetworkHeader header = (BallastFloraBehavior.NetworkHeader) message.ReadByte(); + if (header == BallastFloraBehavior.NetworkHeader.Spawn) + { + string identifier = message.ReadString(); + float x = message.ReadSingle(); + float y = message.ReadSingle(); + BallastFlora = new BallastFloraBehavior(this, BallastFloraPrefab.Find(identifier), new Vector2(x, y), firstGrowth: true) + { + PowerConsumptionTimer = message.ReadSingle() + }; + } + else if (BallastFlora != null) + { + BallastFlora.ClientRead(message, header); + } + return; + } remoteWaterVolume = message.ReadRangedSingle(0.0f, 1.5f, 8) * Volume; remoteOxygenPercentage = message.ReadRangedSingle(0.0f, 100.0f, 8); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreature.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreature.cs index 113c73335..ad29039b0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreature.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreature.cs @@ -1,30 +1,38 @@ -using Microsoft.Xna.Framework; +using Barotrauma.SpriteDeformations; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; +using System.Xml.Linq; namespace Barotrauma { class BackgroundCreature : ISteerable { - const float MaxDepth = 100.0f; + const float MaxDepth = 10000.0f; const float CheckWallsInterval = 5.0f; - public bool Enabled; + public bool Visible; - private BackgroundCreaturePrefab prefab; + public readonly BackgroundCreaturePrefab Prefab; + private readonly List uniqueSpriteDeformations = new List(); + private readonly List spriteDeformations = new List(); + private readonly List lightSpriteDeformations = new List(); + private Vector2 position; private Vector3 velocity; private float depth; - - private SteeringManager steeringManager; - private float checkWallsTimer; + private float alpha = 1.0f; + + private readonly SteeringManager steeringManager; + + private float checkWallsTimer, flashTimer; private float wanderZPhase; private Vector2 obstacleDiff; @@ -33,9 +41,16 @@ namespace Barotrauma public Swarm Swarm; Vector2 drawPosition; - public Vector2 TransformedPosition + + public Vector2[,] CurrentSpriteDeformation { - get { return drawPosition; } + get; + private set; + } + public Vector2[,] CurrentLightSpriteDeformation + { + get; + private set; } public Vector2 SimPosition @@ -58,11 +73,10 @@ namespace Barotrauma get; set; } - + public BackgroundCreature(BackgroundCreaturePrefab prefab, Vector2 position) { - this.prefab = prefab; - + this.Prefab = prefab; this.position = position; drawPosition = position; @@ -70,18 +84,73 @@ namespace Barotrauma steeringManager = new SteeringManager(this); velocity = new Vector3( - Rand.Range(-prefab.Speed, prefab.Speed), - Rand.Range(-prefab.Speed, prefab.Speed), - Rand.Range(0.0f, prefab.WanderZAmount)); + Rand.Range(-prefab.Speed, prefab.Speed, Rand.RandSync.ClientOnly), + Rand.Range(-prefab.Speed, prefab.Speed, Rand.RandSync.ClientOnly), + Rand.Range(0.0f, prefab.WanderZAmount, Rand.RandSync.ClientOnly)); - checkWallsTimer = Rand.Range(0.0f, CheckWallsInterval); + checkWallsTimer = Rand.Range(0.0f, CheckWallsInterval, Rand.RandSync.ClientOnly); + foreach (XElement subElement in prefab.Config.Elements()) + { + List deformationList = null; + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "deformablesprite": + deformationList = spriteDeformations; + break; + case "deformablelightsprite": + deformationList = lightSpriteDeformations; + break; + default: + continue; + } + foreach (XElement animationElement in subElement.Elements()) + { + SpriteDeformation deformation = null; + int sync = animationElement.GetAttributeInt("sync", -1); + if (sync > -1) + { + string typeName = animationElement.GetAttributeString("type", "").ToLowerInvariant(); + deformation = uniqueSpriteDeformations.Find(d => d.TypeName == typeName && d.Sync == sync); + } + if (deformation == null) + { + deformation = SpriteDeformation.Load(animationElement, prefab.Name); + if (deformation != null) + { + uniqueSpriteDeformations.Add(deformation); + } + } + if (deformation != null) + { + deformationList.Add(deformation); + } + } + } } public void Update(float deltaTime) { position += new Vector2(velocity.X, velocity.Y) * deltaTime; - depth = MathHelper.Clamp(depth + velocity.Z * deltaTime, 0.0f, MaxDepth); + depth = MathHelper.Clamp(depth + velocity.Z * deltaTime, Prefab.MinDepth, Prefab.MaxDepth * 10); + + if (Prefab.FlashInterval > 0.0f) + { + flashTimer -= deltaTime; + if (flashTimer > 0.0f) + { + alpha = 0.0f; + } + else + { + //value goes from 0 to 1 and back to 0 during the flash + alpha = (float)Math.Sin(-flashTimer / Prefab.FlashDuration * MathHelper.Pi) * PerlinNoise.GetPerlin((float)Timing.TotalTime * 0.1f, (float)Timing.TotalTime * 0.2f); + if (flashTimer < -Prefab.FlashDuration) + { + flashTimer = Prefab.FlashInterval; + } + } + } checkWallsTimer -= deltaTime; if (checkWallsTimer <= 0.0f && Level.Loaded != null) @@ -134,54 +203,145 @@ namespace Barotrauma float midPointDist = Vector2.Distance(SimPosition, midPoint) * 100.0f; if (midPointDist > Swarm.MaxDistance) { - steeringManager.SteeringSeek(midPoint, ((midPointDist / Swarm.MaxDistance) - 1.0f) * prefab.Speed); + steeringManager.SteeringSeek(midPoint, ((midPointDist / Swarm.MaxDistance) - 1.0f) * Prefab.Speed); } steeringManager.SteeringManual(deltaTime, Swarm.AvgVelocity() * Swarm.Cohesion); } - if (prefab.WanderAmount > 0.0f) + if (Prefab.WanderAmount > 0.0f) { - steeringManager.SteeringWander(prefab.Speed); + steeringManager.SteeringWander(Prefab.Speed); } if (obstacleDiff != Vector2.Zero) { - steeringManager.SteeringManual(deltaTime, -obstacleDiff * (1.0f - obstacleDist / 5000.0f) * prefab.Speed); + steeringManager.SteeringManual(deltaTime, -obstacleDiff * (1.0f - obstacleDist / 5000.0f) * Prefab.Speed); } - steeringManager.Update(prefab.Speed); + steeringManager.Update(Prefab.Speed); - if (prefab.WanderZAmount > 0.0f) + if (Prefab.WanderZAmount > 0.0f) { - wanderZPhase += Rand.Range(-prefab.WanderZAmount, prefab.WanderZAmount); - velocity.Z = (float)Math.Sin(wanderZPhase) * prefab.Speed; + wanderZPhase += Rand.Range(-Prefab.WanderZAmount, Prefab.WanderZAmount); + velocity.Z = (float)Math.Sin(wanderZPhase) * Prefab.Speed; } velocity = Vector3.Lerp(velocity, new Vector3(Steering.X, Steering.Y, velocity.Z), deltaTime); + + UpdateDeformations(deltaTime); + } + + public void DrawLightSprite(SpriteBatch spriteBatch, Camera cam) + { + Draw(spriteBatch, cam, Prefab.LightSprite, Prefab.DeformableLightSprite, CurrentLightSpriteDeformation, Color.White * alpha); } public void Draw(SpriteBatch spriteBatch, Camera cam) { + Draw(spriteBatch, + cam, + Prefab.Sprite, + Prefab.DeformableSprite, + CurrentSpriteDeformation, + Color.Lerp(Color.White, Level.Loaded.BackgroundColor, depth / Math.Max(MaxDepth, Prefab.MaxDepth)) * alpha); + } + + private void Draw(SpriteBatch spriteBatch, Camera cam, Sprite sprite, DeformableSprite deformableSprite, Vector2[,] currentSpriteDeformation, Color color) + { + if (sprite == null && deformableSprite == null) { return; } + if (color.A == 0) { return; } + float rotation = 0.0f; - if (!prefab.DisableRotation) + if (!Prefab.DisableRotation) { rotation = MathUtils.VectorToAngle(new Vector2(velocity.X, -velocity.Y)); - if (velocity.X < 0.0f) rotation -= MathHelper.Pi; + if (velocity.X < 0.0f) { rotation -= MathHelper.Pi; } } - drawPosition = position; - if (depth > 0.0f) + drawPosition = GetDrawPosition(cam); + + float scale = GetScale(); + sprite?.Draw(spriteBatch, + new Vector2(drawPosition.X, -drawPosition.Y), + color, + rotation, + scale, + Prefab.DisableFlipping || velocity.X > 0.0f ? SpriteEffects.None : SpriteEffects.FlipHorizontally, + Math.Min(depth / MaxDepth, 1.0f)); + + if (deformableSprite != null) + { + if (currentSpriteDeformation != null) + { + deformableSprite.Deform(currentSpriteDeformation); + } + else + { + deformableSprite.Reset(); + } + deformableSprite?.Draw(cam, + new Vector3(drawPosition.X, drawPosition.Y, Math.Min(depth / 10000.0f, 1.0f)), + deformableSprite.Origin, + rotation, + Vector2.One * scale, + color, + mirror: Prefab.DisableFlipping || velocity.X <= 0.0f); + } + } + + public Vector2 GetDrawPosition(Camera cam) + { + Vector2 drawPosition = WorldPosition; + if (depth >= 0) { Vector2 camOffset = drawPosition - cam.WorldViewCenter; - drawPosition -= camOffset * (depth / MaxDepth) * 0.05f; + drawPosition -= camOffset * depth / MaxDepth; } + return drawPosition; + } - prefab.Sprite.Draw(spriteBatch, - new Vector2(drawPosition.X, -drawPosition.Y), - Color.Lerp(Color.White, Level.Loaded.BackgroundColor, (depth / MaxDepth) * 0.2f), - rotation, (1.0f - (depth / MaxDepth) * 0.2f) * prefab.Scale, - velocity.X > 0.0f ? SpriteEffects.None : SpriteEffects.FlipHorizontally, - (depth / MaxDepth)); + public float GetScale() + { + return Math.Max(1.0f - depth / MaxDepth, 0.05f) * Prefab.Scale; + } + + public Rectangle GetExtents(Camera cam) + { + Vector2 min = GetDrawPosition(cam); + Vector2 max = min; + + float scale = GetScale(); + GetSpriteExtents(Prefab.Sprite, ref min, ref max); + GetSpriteExtents(Prefab.LightSprite, ref min, ref max); + GetSpriteExtents(Prefab.DeformableSprite?.Sprite, ref min, ref max); + GetSpriteExtents(Prefab.DeformableLightSprite?.Sprite, ref min, ref max); + + return new Rectangle(min.ToPoint(), (max - min).ToPoint()); + + void GetSpriteExtents(Sprite sprite, ref Vector2 min, ref Vector2 max) + { + if (sprite == null) { return; } + min.X = Math.Min(min.X, min.X - sprite.size.X * sprite.RelativeOrigin.X * scale); + min.Y = Math.Min(min.Y, min.Y - sprite.size.Y * sprite.RelativeOrigin.Y * scale); + max.X = Math.Max(max.X, max.X + sprite.size.X * (1.0f - sprite.RelativeOrigin.X) * scale); + max.Y = Math.Max(max.Y, max.Y + sprite.size.Y * (1.0f - sprite.RelativeOrigin.Y) * scale); + } + } + + private void UpdateDeformations(float deltaTime) + { + foreach (SpriteDeformation deformation in uniqueSpriteDeformations) + { + deformation.Update(deltaTime); + } + if (spriteDeformations.Count > 0) + { + CurrentSpriteDeformation = SpriteDeformation.GetDeformation(spriteDeformations, Prefab.DeformableSprite.Size); + } + if (lightSpriteDeformations.Count > 0) + { + CurrentLightSpriteDeformation = SpriteDeformation.GetDeformation(lightSpriteDeformations, Prefab.DeformableLightSprite.Size); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreatureManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreatureManager.cs index 2252ed9b5..70d2d2358 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreatureManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreatureManager.cs @@ -9,14 +9,14 @@ namespace Barotrauma { class BackgroundCreatureManager { - const int MaxSprites = 100; + const int MaxCreatures = 100; - const float CheckActiveInterval = 1.0f; + const float VisibilityCheckInterval = 1.0f; - private float checkActiveTimer; + private float checkVisibleTimer; - private List prefabs = new List(); - private List activeSprites = new List(); + private readonly List prefabs = new List(); + private readonly List creatures = new List(); public BackgroundCreatureManager(string configPath) { @@ -60,92 +60,111 @@ namespace Barotrauma } } - public void SpawnSprites(int count, Vector2? position = null) + public void SpawnCreatures(Level level, int count, Vector2? position = null) { - activeSprites.Clear(); + creatures.Clear(); - if (prefabs.Count == 0) return; + if (prefabs.Count == 0) { return; } - count = Math.Min(count, MaxSprites); + count = Math.Min(count, MaxCreatures); - for (int i = 0; i < count; i++ ) + List availablePrefabs = new List(prefabs); + + for (int i = 0; i < count; i++) { Vector2 pos = Vector2.Zero; - if (position == null) { - var wayPoints = WayPoint.WayPointList.FindAll(wp => wp.Submarine==null); + var wayPoints = WayPoint.WayPointList.FindAll(wp => wp.Submarine == null); if (wayPoints.Any()) { WayPoint wp = wayPoints[Rand.Int(wayPoints.Count, Rand.RandSync.ClientOnly)]; - pos = new Vector2(wp.Rect.X, wp.Rect.Y); pos += Rand.Vector(200.0f, Rand.RandSync.ClientOnly); } else { pos = Rand.Vector(2000.0f, Rand.RandSync.ClientOnly); - } + } } else { pos = (Vector2)position; } - - var prefab = prefabs[Rand.Int(prefabs.Count, Rand.RandSync.ClientOnly)]; + var prefab = ToolBox.SelectWeightedRandom(availablePrefabs, availablePrefabs.Select(p => p.GetCommonness(level.GenerationParams)).ToList(), Rand.RandSync.ClientOnly); + if (prefab == null) { break; } int amount = Rand.Range(prefab.SwarmMin, prefab.SwarmMax, Rand.RandSync.ClientOnly); List swarmMembers = new List(); - for (int n = 0; n < amount; n++) { - var newSprite = new BackgroundCreature(prefab, pos); - activeSprites.Add(newSprite); - swarmMembers.Add(newSprite); + var creature = new BackgroundCreature(prefab, pos + Rand.Vector(Rand.Range(0.0f, prefab.SwarmRadius, Rand.RandSync.ClientOnly), Rand.RandSync.ClientOnly)); + creatures.Add(creature); + swarmMembers.Add(creature); } - if (amount > 0) + if (amount > 1) { new Swarm(swarmMembers, prefab.SwarmRadius, prefab.SwarmCohesion); } + if (creatures.Count(c => c.Prefab == prefab) > prefab.MaxCount) + { + availablePrefabs.Remove(prefab); + if (availablePrefabs.Count <= 0) { break; } + } } } - public void ClearSprites() + public void Clear() { - activeSprites.Clear(); + creatures.Clear(); } public void Update(float deltaTime, Camera cam) { - if (checkActiveTimer < 0.0f) + if (checkVisibleTimer < 0.0f) { - foreach (BackgroundCreature sprite in activeSprites) + int margin = 500; + foreach (BackgroundCreature creature in creatures) { - sprite.Enabled = Math.Abs(sprite.TransformedPosition.X - cam.WorldViewCenter.X) < cam.WorldView.Width && - Math.Abs(sprite.TransformedPosition.Y - cam.WorldViewCenter.Y) < cam.WorldView.Height; + Rectangle extents = creature.GetExtents(cam); + bool wasVisible = creature.Visible; + creature.Visible = + extents.Right >= cam.WorldView.X - margin && + extents.X <= cam.WorldView.Right + margin && + extents.Bottom >= cam.WorldView.Y - cam.WorldView.Height - margin && + extents.Y <= cam.WorldView.Y + margin; } - checkActiveTimer = CheckActiveInterval; + checkVisibleTimer = VisibilityCheckInterval; } else { - checkActiveTimer -= deltaTime; + checkVisibleTimer -= deltaTime; } - foreach (BackgroundCreature sprite in activeSprites) + foreach (BackgroundCreature creature in creatures) { - if (!sprite.Enabled) continue; - sprite.Update(deltaTime); + if (!creature.Visible) { continue; } + creature.Update(deltaTime); } } public void Draw(SpriteBatch spriteBatch, Camera cam) { - foreach (BackgroundCreature sprite in activeSprites) + foreach (BackgroundCreature creature in creatures) { - if (!sprite.Enabled) continue; - sprite.Draw(spriteBatch, cam); + if (!creature.Visible) { continue; } + creature.Draw(spriteBatch, cam); + } + } + + public void DrawLights(SpriteBatch spriteBatch, Camera cam) + { + foreach (BackgroundCreature creature in creatures) + { + if (!creature.Visible) { continue; } + creature.DrawLightSprite(spriteBatch, cam); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreaturePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreaturePrefab.cs index 94501138a..30939a39a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreaturePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreaturePrefab.cs @@ -1,50 +1,117 @@ -using System.Xml.Linq; +using System.Collections.Generic; +using System.Xml.Linq; namespace Barotrauma { class BackgroundCreaturePrefab { - public readonly Sprite Sprite; + public readonly Sprite Sprite, LightSprite; + public readonly DeformableSprite DeformableSprite, DeformableLightSprite; - public readonly float Speed; + public readonly string Name; - public readonly float WanderAmount; + public readonly XElement Config; - public readonly float WanderZAmount; + [Serialize(1.0f, true)] + public float Speed { get; private set; } - public readonly int SwarmMin, SwarmMax; - public readonly float SwarmRadius, SwarmCohesion; + [Serialize(0.0f, true)] + public float WanderAmount { get; private set; } - public readonly bool DisableRotation; + [Serialize(0.0f, true)] + public float WanderZAmount { get; private set; } + + [Serialize(1, true)] + public int SwarmMin { get; private set; } + + [Serialize(1, true)] + public int SwarmMax { get; private set; } + + [Serialize(200.0f, true)] + public float SwarmRadius { get; private set; } + + [Serialize(0.2f, true)] + public float SwarmCohesion { get; private set; } + + [Serialize(10.0f, true)] + public float MinDepth { get; private set; } + + [Serialize(1000.0f, true)] + public float MaxDepth { get; private set; } + + [Serialize(false, true)] + public bool DisableRotation { get; private set; } + + [Serialize(false, true)] + public bool DisableFlipping { get; private set; } + + [Serialize(1.0f, true)] + public float Scale { get; private set; } + + [Serialize(1.0f, true)] + public float Commonness { get; private set; } + + [Serialize(1000, true)] + public int MaxCount { get; private set; } + + [Serialize(0.0f, true)] + public float FlashInterval { get; private set; } + + [Serialize(0.0f, true)] + public float FlashDuration { get; private set; } + + + /// + /// Overrides the commonness of the object in a specific level type. + /// Key = name of the level type, value = commonness in that level type. + /// + public Dictionary OverrideCommonness = new Dictionary(); - public readonly float Scale; - public BackgroundCreaturePrefab(XElement element) { - Speed = element.GetAttributeFloat("speed", 1.0f); + Name = element.Name.ToString(); - WanderAmount = element.GetAttributeFloat("wanderamount", 0.0f); + Config = element; - WanderZAmount = element.GetAttributeFloat("wanderzamount", 0.0f); - - SwarmMin = element.GetAttributeInt("swarmmin", 1); - SwarmMax = element.GetAttributeInt("swarmmax", 1); - - SwarmRadius = element.GetAttributeFloat("swarmradius", 200.0f); - SwarmCohesion = element.GetAttributeFloat("swarmcohesion", 0.2f); - - DisableRotation = element.GetAttributeBool("disablerotation", false); - - Scale = element.GetAttributeFloat("scale", 1.0f); + SerializableProperty.DeserializeProperties(this, element); foreach (XElement subElement in element.Elements()) { - if (!subElement.Name.ToString().Equals("sprite", System.StringComparison.OrdinalIgnoreCase)) { continue; } - - Sprite = new Sprite(subElement, lazyLoad: true); - break; + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "sprite": + Sprite = new Sprite(subElement, lazyLoad: true); + break; + case "deformablesprite": + DeformableSprite = new DeformableSprite(subElement, lazyLoad: true); + break; + case "lightsprite": + LightSprite = new Sprite(subElement, lazyLoad: true); + break; + case "deformablelightsprite": + DeformableLightSprite = new DeformableSprite(subElement, lazyLoad: true); + break; + case "overridecommonness": + string levelType = subElement.GetAttributeString("leveltype", "").ToLowerInvariant(); + if (!OverrideCommonness.ContainsKey(levelType)) + { + OverrideCommonness.Add(levelType, subElement.GetAttributeFloat("commonness", 1.0f)); + } + break; + } } } + + public float GetCommonness(LevelGenerationParams generationParams) + { + if (generationParams?.Identifier != null && + (OverrideCommonness.TryGetValue(generationParams.Identifier, out float commonness) || + (generationParams.OldIdentifier != null && OverrideCommonness.TryGetValue(generationParams.OldIdentifier, out commonness)))) + { + return commonness; + } + return Commonness; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/CaveGenerator.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/CaveGenerator.cs index 6f2aaae51..7fa3a7021 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/CaveGenerator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/CaveGenerator.cs @@ -1,4 +1,5 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; @@ -8,72 +9,82 @@ namespace Barotrauma { static partial class CaveGenerator { - public static List GenerateRenderVerticeList(List triangles) + public static List GenerateWallVertices(List triangles, LevelGenerationParams generationParams, float zCoord) { - var verticeList = new List(); + var vertices = new List(); for (int i = 0; i < triangles.Count; i++) { foreach (Vector2 vertex in triangles[i]) { - //shift the coordinates around a bit to make the texture repetition less obvious - Vector2 uvCoords = new Vector2( - vertex.X / 2000.0f + (float)Math.Sin(vertex.X / 500.0f) * 0.15f, - vertex.Y / 2000.0f + (float)Math.Sin(vertex.Y / 700.0f) * 0.15f); - - verticeList.Add(new VertexPositionTexture(new Vector3(vertex, 1.0f), uvCoords)); + Vector2 uvCoords = vertex / generationParams.WallTextureSize; + vertices.Add(new VertexPositionTexture(new Vector3(vertex, zCoord), uvCoords)); } } - return verticeList; + return vertices; } - public static VertexPositionTexture[] GenerateWallShapes(List cells, Level level) + public static List GenerateWallEdgeVertices(List cells, Level level, float zCoord) { - float outWardThickness = 30.0f; + float outWardThickness = level.GenerationParams.WallEdgeExpandOutwardsAmount; - List verticeList = new List(); + List vertices = new List(); foreach (VoronoiCell cell in cells) { - CompareCCW compare = new CompareCCW(cell.Center); + Vector2 minVert = cell.Edges[0].Point1; + Vector2 maxVert = cell.Edges[0].Point1; + float circumference = 0.0f; foreach (GraphEdge edge in cell.Edges) { - if (edge.Cell1 != null && edge.Cell1.Body == null && edge.Cell1.CellType != CellType.Empty) edge.Cell1 = null; - if (edge.Cell2 != null && edge.Cell2.Body == null && edge.Cell2.CellType != CellType.Empty) edge.Cell2 = null; - - if (compare.Compare(edge.Point1, edge.Point2) == -1) - { - var temp = edge.Point1; - edge.Point1 = edge.Point2; - edge.Point2 = temp; - } + 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)); } - } - - foreach (VoronoiCell cell in cells) - { + Vector2 center = (minVert + maxVert) / 2; foreach (GraphEdge edge in cell.Edges) { - if (!edge.IsSolid) continue; + if (!edge.IsSolid) { continue; } - GraphEdge leftEdge = cell.Edges.Find(e => e != edge && (edge.Point1 == e.Point1 || edge.Point1 == e.Point2)); - GraphEdge rightEdge = cell.Edges.Find(e => e != edge && (edge.Point2 == e.Point1 || edge.Point2 == e.Point2)); + GraphEdge leftEdge = cell.Edges.Find(e => e != edge && (edge.Point1.NearlyEquals(e.Point1) || edge.Point1.NearlyEquals(e.Point2))); + var leftAdjacentCell = leftEdge?.AdjacentCell(cell); + if (leftAdjacentCell != null) + { + var adjEdge = leftAdjacentCell.Edges.Find(e => e != leftEdge && e.IsSolid && (edge.Point1.NearlyEquals(e.Point1) || edge.Point1.NearlyEquals(e.Point2))); + if (adjEdge != null) { leftEdge = adjEdge; } + } + + GraphEdge rightEdge = cell.Edges.Find(e => e != edge && (edge.Point2.NearlyEquals(e.Point1) || edge.Point2.NearlyEquals(e.Point2))); + var rightAdjacentCell = rightEdge?.AdjacentCell(cell); + if (rightAdjacentCell != null) + { + var adjEdge = rightAdjacentCell.Edges.Find(e => e != rightEdge && e.IsSolid && (edge.Point2.NearlyEquals(e.Point1) || edge.Point2.NearlyEquals(e.Point2))); + if (adjEdge != null) { rightEdge = adjEdge; } + } Vector2 leftNormal = Vector2.Zero, rightNormal = Vector2.Zero; - float inwardThickness1 = 100; - float inwardThickness2 = 100; + float inwardThickness1 = level.GenerationParams.WallEdgeExpandInwardsAmount; + float inwardThickness2 = level.GenerationParams.WallEdgeExpandInwardsAmount; if (leftEdge != null && !leftEdge.IsSolid) { - leftNormal = edge.Point1 == leftEdge.Point1 ? + leftNormal = edge.Point1.NearlyEquals(leftEdge.Point1) ? Vector2.Normalize(leftEdge.Point2 - leftEdge.Point1) : Vector2.Normalize(leftEdge.Point1 - leftEdge.Point2); - inwardThickness1 = Vector2.Distance(leftEdge.Point1, leftEdge.Point2) / 2; + } + else if (leftEdge != null) + { + leftNormal = -Vector2.Normalize(edge.GetNormal(cell) + leftEdge.GetNormal(leftAdjacentCell ?? cell)); + if (!MathUtils.IsValid(leftNormal)) { leftNormal = -edge.GetNormal(cell); } } else { leftNormal = Vector2.Normalize(cell.Center - edge.Point1); - inwardThickness1 = Vector2.Distance(edge.Point1, cell.Center) / 2; } + inwardThickness1 = Math.Min(Vector2.Distance(edge.Point1, cell.Center), inwardThickness1); if (!MathUtils.IsValid(leftNormal)) { @@ -86,7 +97,7 @@ namespace Barotrauma if (cell.Body != null) { - GameMain.World.Remove(cell.Body); + if (GameMain.World.BodyList.Contains(cell.Body)) { GameMain.World.Remove(cell.Body); } cell.Body = null; } leftNormal = Vector2.UnitX; @@ -95,16 +106,20 @@ namespace Barotrauma if (rightEdge != null && !rightEdge.IsSolid) { - rightNormal = edge.Point2 == rightEdge.Point1 ? + rightNormal = edge.Point2.NearlyEquals(rightEdge.Point1) ? Vector2.Normalize(rightEdge.Point2 - rightEdge.Point1) : Vector2.Normalize(rightEdge.Point1 - rightEdge.Point2); - inwardThickness2 = Vector2.Distance(rightEdge.Point1, rightEdge.Point2) / 2; + } + else if (rightEdge != null) + { + rightNormal = -Vector2.Normalize(edge.GetNormal(cell) + rightEdge.GetNormal(rightAdjacentCell ?? cell)); + if (!MathUtils.IsValid(rightNormal)) { rightNormal = -edge.GetNormal(cell); } } else { rightNormal = Vector2.Normalize(cell.Center - edge.Point2); - inwardThickness2 = Vector2.Distance(edge.Point2, cell.Center) / 2; } + inwardThickness2 = Math.Min(Vector2.Distance(edge.Point2, cell.Center), inwardThickness2); if (!MathUtils.IsValid(rightNormal)) { @@ -117,24 +132,23 @@ namespace Barotrauma if (cell.Body != null) { - GameMain.World.Remove(cell.Body); + if (GameMain.World.BodyList.Contains(cell.Body)) { GameMain.World.Remove(cell.Body); } cell.Body = null; } rightNormal = Vector2.UnitX; 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) - { - point2UV += MathHelper.TwoPi; + if (point1UV - point2UV > MathHelper.Pi) + { + point1UV -= MathHelper.TwoPi; } - //the texture wraps around the cell 4 times - //TODO: define the uv scale in level generation parameters? - point1UV = point1UV / MathHelper.TwoPi * 4; - point2UV = point2UV / MathHelper.TwoPi * 4; + int textureRepeatCount = (int)Math.Max(circumference / 2 / level.GenerationParams.WallEdgeTextureWidth, 1); + point1UV = point1UV / MathHelper.TwoPi * textureRepeatCount; + point2UV = point2UV / MathHelper.TwoPi * textureRepeatCount; for (int i = 0; i < 2; i++) { @@ -147,9 +161,9 @@ namespace Barotrauma verts[1] = edge.Point2 - rightNormal * outWardThickness; verts[2] = edge.Point1 + leftNormal * inwardThickness1; - vertPos[0] = new VertexPositionTexture(new Vector3(verts[0], 0.0f), new Vector2(point1UV, 0.0f)); - vertPos[1] = new VertexPositionTexture(new Vector3(verts[1], 0.0f), new Vector2(point2UV, 0.0f)); - vertPos[2] = new VertexPositionTexture(new Vector3(verts[2], 0.0f), new Vector2(point1UV, 0.5f)); + vertPos[0] = new VertexPositionTexture(new Vector3(verts[0], zCoord), new Vector2(point1UV, 0.0f)); + vertPos[1] = new VertexPositionTexture(new Vector3(verts[1], zCoord), new Vector2(point2UV, 0.0f)); + vertPos[2] = new VertexPositionTexture(new Vector3(verts[2], zCoord), new Vector2(point1UV, 1.0f)); } else { @@ -158,16 +172,16 @@ namespace Barotrauma verts[1] = edge.Point2 - rightNormal * outWardThickness; verts[2] = edge.Point2 + rightNormal * inwardThickness2; - vertPos[0] = new VertexPositionTexture(new Vector3(verts[0], 0.0f), new Vector2(point1UV, 0.5f)); - vertPos[1] = new VertexPositionTexture(new Vector3(verts[1], 0.0f), new Vector2(point2UV, 0.0f)); - vertPos[2] = new VertexPositionTexture(new Vector3(verts[2], 0.0f), new Vector2(point2UV, 0.5f)); + vertPos[0] = new VertexPositionTexture(new Vector3(verts[0], zCoord), new Vector2(point1UV, 1.0f)); + vertPos[1] = new VertexPositionTexture(new Vector3(verts[1], zCoord), new Vector2(point2UV, 0.0f)); + vertPos[2] = new VertexPositionTexture(new Vector3(verts[2], zCoord), new Vector2(point2UV, 1.0f)); } - verticeList.AddRange(vertPos); + vertices.AddRange(vertPos); } } } - return verticeList.ToArray(); + return vertices; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/DestructibleLevelWall.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/DestructibleLevelWall.cs new file mode 100644 index 000000000..cde165b2a --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/DestructibleLevelWall.cs @@ -0,0 +1,62 @@ +using Microsoft.Xna.Framework; +using System; +using System.Linq; + +namespace Barotrauma +{ + partial class DestructibleLevelWall : LevelWall, IDamageable + { + + public override float Alpha + { + get + { + if (FadeOutDuration <= 0.0f || FadeOutTimer < FadeOutDuration - 1.0f) { return 1.0f; } + return MathHelper.Clamp(FadeOutDuration - FadeOutTimer, 0.0f, 1.0f); + } + } + + partial void AddDamageProjSpecific(float damage, Vector2 worldPosition) + { + if (damage <= 0.0f) { return; } + Vector2 particlePos = worldPosition; + if (!Cells.Any(c => c.IsPointInside(particlePos))) + { + bool intersectionFound = false; + foreach (var cell in Cells) + { + foreach (var edge in cell.Edges) + { + if (MathUtils.GetLineIntersection(worldPosition, cell.Center, edge.Point1 + cell.Translation, edge.Point2 + cell.Translation, out Vector2 intersection)) + { + intersectionFound = true; + particlePos = intersection; + break; + } + } + if (intersectionFound) { break; } + } + } + + Vector2 particleDir = particlePos - WorldPosition; + if (particleDir.LengthSquared() > 0.0001f) { particleDir = Vector2.Normalize(particleDir); } + int particleAmount = MathHelper.Clamp((int)damage, 1, 10); + for (int i = 0; i < particleAmount; i++) + { + var particle = GameMain.ParticleManager.CreateParticle("iceshards", + particlePos + Rand.Vector(5.0f), + particleDir * Rand.Range(200.0f, 500.0f) + Rand.Vector(100.0f)); + } + } + + public void SetDamage(float damage) + { + Damage = damage; + if (Damage >= MaxHealth && !Destroyed) + { + CreateFragments(); + Destroy(); + } + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs index 506f1387d..4bf8e79fb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs @@ -14,6 +14,8 @@ namespace Barotrauma private BackgroundCreatureManager backgroundCreatureManager; + public BackgroundCreatureManager BackgroundCreatureManager => backgroundCreatureManager; + public LevelRenderer Renderer => renderer; public void ReloadTextures() @@ -33,14 +35,6 @@ namespace Barotrauma uniqueSprites.Add(sprite); } } - foreach (Sprite specularSprite in levelObj.Prefab.SpecularSprites) - { - if (!uniqueTextures.Contains(specularSprite.Texture)) - { - uniqueTextures.Add(specularSprite.Texture); - uniqueSprites.Add(specularSprite); - } - } } foreach (Sprite sprite in uniqueSprites) @@ -51,7 +45,7 @@ namespace Barotrauma public void DrawFront(SpriteBatch spriteBatch, Camera cam) { - if (renderer == null) return; + if (renderer == null) { return; } renderer.Draw(spriteBatch, cam); if (GameMain.DebugDraw && Screen.Selected.Cam.Zoom > 0.1f) @@ -123,22 +117,34 @@ namespace Barotrauma if (renderer == null) return; renderer.DrawBackground(spriteBatch, cam, LevelObjectManager, backgroundCreatureManager); } - + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { - foreach (LevelWall levelWall in ExtraWalls) + bool isGlobalUpdate = msg.ReadBoolean(); + if (isGlobalUpdate) { - if (levelWall.Body.BodyType == BodyType.Static) continue; - - Vector2 bodyPos = new Vector2( - msg.ReadSingle(), - msg.ReadSingle()); - - levelWall.MoveState = msg.ReadRangedSingle(0.0f, MathHelper.TwoPi, 16); - - if (Vector2.DistanceSquared(bodyPos, levelWall.Body.Position) > 0.5f) + foreach (LevelWall levelWall in ExtraWalls) { - levelWall.Body.SetTransformIgnoreContacts(ref bodyPos, levelWall.Body.Rotation); + if (levelWall.Body.BodyType == BodyType.Static) { continue; } + + Vector2 bodyPos = new Vector2( + msg.ReadSingle(), + msg.ReadSingle()); + levelWall.MoveState = msg.ReadRangedSingle(0.0f, MathHelper.TwoPi, 16); + DestructibleLevelWall destructibleWall = levelWall as DestructibleLevelWall; + if (Vector2.DistanceSquared(bodyPos, levelWall.Body.Position) > 0.5f && (destructibleWall == null || !destructibleWall.Destroyed)) + { + levelWall.Body.SetTransformIgnoreContacts(ref bodyPos, levelWall.Body.Rotation); + } + } + } + else + { + int index = msg.ReadUInt16(); + byte damageByte = msg.ReadByte(); + if (index < ExtraWalls.Count && ExtraWalls[index] is DestructibleLevelWall destructibleWall) + { + destructibleWall.SetDamage(destructibleWall.MaxHealth * damageByte / 255.0f); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs index fdc5a0afe..ca4822b95 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs @@ -1,14 +1,14 @@ using Barotrauma.Lights; +using Barotrauma.Networking; using Barotrauma.Particles; using Barotrauma.Sounds; -using Barotrauma.Networking; +using Barotrauma.SpriteDeformations; +using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.Xml.Linq; -using Barotrauma.SpriteDeformations; using System.Linq; -using FarseerPhysics.Dynamics; +using System.Xml.Linq; namespace Barotrauma { @@ -74,10 +74,21 @@ namespace Barotrauma private set; } + public bool VisibleOnSonar + { + get; + private set; + } + + public float SonarRadius + { + get; + private set; + } + partial void InitProjSpecific() { Sprite?.EnsureLazyLoaded(); - SpecularSprite?.EnsureLazyLoaded(); Prefab.DeformableSprite?.EnsureLazyLoaded(); CurrentSwingAmount = Prefab.SwingAmountRad; @@ -98,7 +109,7 @@ namespace Barotrauma } } - if (Prefab.LightSourceParams != null) + if (Prefab.LightSourceParams != null && Prefab.LightSourceParams.Count > 0) { LightSources = new LightSource[Prefab.LightSourceParams.Count]; LightSourceTriggers = new LevelTrigger[Prefab.LightSourceParams.Count]; @@ -138,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) @@ -232,17 +250,21 @@ namespace Barotrauma deformation.Update(deltaTime); } CurrentSpriteDeformation = SpriteDeformation.GetDeformation(spriteDeformations, ActivePrefab.DeformableSprite.Size); - foreach (LightSource lightSource in LightSources) + if (LightSources != null) { - if (lightSource?.DeformableLightSprite != null) + foreach (LightSource lightSource in LightSources) { - lightSource.DeformableLightSprite.Deform(CurrentSpriteDeformation); + if (lightSource?.DeformableLightSprite != null) + { + lightSource.DeformableLightSprite.Deform(CurrentSpriteDeformation); + } } } } private void UpdatePositionalDeformation(PositionalDeformation positionalDeformation, float deltaTime) { + if (Triggers == null) { return; } Matrix matrix = ActivePrefab.DeformableSprite.GetTransform( Position, ActivePrefab.DeformableSprite.Origin, @@ -258,7 +280,7 @@ namespace Barotrauma Vector2 moveAmount = triggerer.WorldPosition - trigger.TriggererPosition[triggerer]; moveAmount = Vector2.Transform(moveAmount, rotationMatrix); - moveAmount /= (ActivePrefab.DeformableSprite.Size * Scale); + moveAmount /= ActivePrefab.DeformableSprite.Size * Scale; moveAmount.Y = -moveAmount.Y; positionalDeformation.Deform(trigger.WorldPosition, moveAmount, deltaTime, Matrix.Invert(matrix) * @@ -269,9 +291,10 @@ namespace Barotrauma public void ClientRead(IReadMessage msg) { + if (Triggers == null) { return; } for (int i = 0; i < Triggers.Count; i++) { - if (!Triggers[i].UseNetworkSyncing) continue; + if (!Triggers[i].UseNetworkSyncing) { continue; } Triggers[i].ClientRead(msg); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs index 9b93770ed..73ff19b6f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -12,6 +12,8 @@ namespace Barotrauma private readonly List visibleObjectsBack = new List(); private readonly List visibleObjectsFront = new List(); + private double NextRefreshTime; + //Maximum number of visible objects drawn at once. Should be large enough to not have an effect during normal gameplay, //but small enough to prevent wrecking performance when zooming out very far const int MaxVisibleObjects = 500; @@ -38,11 +40,13 @@ namespace Barotrauma /// /// Checks which level objects are in camera view and adds them to the visibleObjects lists /// - private void RefreshVisibleObjects(Rectangle currentIndices) + private void RefreshVisibleObjects(Rectangle currentIndices, float zoom) { visibleObjectsBack.Clear(); visibleObjectsFront.Clear(); + float minSizeToDraw = MathHelper.Lerp(10.0f, 5.0f, Math.Min(zoom * 20.0f, 1.0f)); + for (int x = currentIndices.X; x <= currentIndices.Width; x++) { for (int y = currentIndices.Y; y <= currentIndices.Height; y++) @@ -50,6 +54,22 @@ namespace Barotrauma if (objectGrid[x, y] == null) { continue; } foreach (LevelObject obj in objectGrid[x, y]) { + if (zoom < 0.05f) + { + //hide if the sprite is very small when zoomed this far out + if ((obj.Sprite != null && Math.Min(obj.Sprite.size.X * zoom, obj.Sprite.size.Y * zoom) < 5.0f) || + (obj.ActivePrefab?.DeformableSprite != null && Math.Min(obj.ActivePrefab.DeformableSprite.Sprite.size.X * zoom, obj.ActivePrefab.DeformableSprite.Sprite.size.Y * zoom) < minSizeToDraw)) + { + continue; + } + + float zCutoff = MathHelper.Lerp(5000.0f, 500.0f, (0.05f - zoom) * 20.0f); + if (obj.Position.Z > zCutoff) + { + continue; + } + } + var objectList = obj.Position.Z >= 0 ? visibleObjectsBack : visibleObjectsFront; int drawOrderIndex = 0; for (int i = 0; i < objectList.Count; i++) @@ -83,7 +103,7 @@ namespace Barotrauma } - public void DrawObjects(SpriteBatch spriteBatch, Camera cam, bool drawFront, bool specular = false) + public void DrawObjects(SpriteBatch spriteBatch, Camera cam, bool drawFront) { Rectangle indices = Rectangle.Empty; indices.X = (int)Math.Floor(cam.WorldView.X / (float)GridSize); @@ -102,9 +122,14 @@ namespace Barotrauma indices.Height = Math.Min(indices.Height, objectGrid.GetLength(1) - 1); float z = 0.0f; - if (currentGridIndices != indices) + if (currentGridIndices != indices && Timing.TotalTime > NextRefreshTime) { - RefreshVisibleObjects(indices); + RefreshVisibleObjects(indices, cam.Zoom); + if (cam.Zoom < 0.1f) + { + //when zoomed very far out, refresh a little less often + NextRefreshTime = Timing.TotalTime + MathHelper.Lerp(1.0f, 0.0f, cam.Zoom * 10.0f); + } } var objectList = drawFront ? visibleObjectsFront : visibleObjectsBack; @@ -113,19 +138,17 @@ namespace Barotrauma Vector2 camDiff = new Vector2(obj.Position.X, obj.Position.Y) - cam.WorldViewCenter; camDiff.Y = -camDiff.Y; - Sprite activeSprite = specular ? obj.SpecularSprite : obj.Sprite; + Sprite activeSprite = obj.Sprite; activeSprite?.Draw( spriteBatch, new Vector2(obj.Position.X, -obj.Position.Y) - camDiff * obj.Position.Z / 10000.0f, - Color.Lerp(Color.White, Level.Loaded.BackgroundTextureColor, obj.Position.Z / 5000.0f), + Color.Lerp(Color.White, Level.Loaded.BackgroundTextureColor, obj.Position.Z / 3000.0f), activeSprite.Origin, obj.CurrentRotation, obj.CurrentScale, SpriteEffects.None, z); - if (specular) continue; - if (obj.ActivePrefab.DeformableSprite != null) { if (obj.CurrentSpriteDeformation != null) @@ -149,6 +172,7 @@ namespace Barotrauma { GUI.DrawRectangle(spriteBatch, new Vector2(obj.Position.X, -obj.Position.Y), new Vector2(10.0f, 10.0f), GUI.Style.Red, true); + if (obj.Triggers == null) { continue; } foreach (LevelTrigger trigger in obj.Triggers) { if (trigger.PhysicsBody == null) continue; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs index dd66c27c5..ebf0a6c6c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs @@ -126,7 +126,6 @@ namespace Barotrauma switch (subElement.Name.ToString().ToLowerInvariant()) { case "childobject": - case "lightsource": subElement.Remove(); break; case "deformablesprite": @@ -141,11 +140,31 @@ namespace Barotrauma } } - foreach (LightSourceParams lightSourceParams in LightSourceParams) + for (int i = 0; i < LightSourceParams.Count; i++) { - var lightElement = new XElement("LightSource"); - SerializableProperty.SerializeProperties(lightSourceParams, lightElement); - element.Add(lightElement); + int elementIndex = 0; + bool wasSaved = false; + foreach (XElement subElement in element.Elements().ToList()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "lightsource": + if (elementIndex == i) + { + SerializableProperty.SerializeProperties(LightSourceParams[i], subElement); + wasSaved = true; + break; + } + elementIndex++; + break; + } + } + if (!wasSaved) + { + var lightElement = new XElement("LightSource"); + SerializableProperty.SerializeProperties(LightSourceParams[i], lightElement); + element.Add(lightElement); + } } foreach (ChildObject childObj in ChildObjects) @@ -162,7 +181,7 @@ namespace Barotrauma foreach (XElement subElement in element.Elements()) { if (subElement.Name.ToString().Equals("overridecommonness", System.StringComparison.OrdinalIgnoreCase) - && subElement.GetAttributeString("leveltype", "") == overrideCommonness.Key) + && subElement.GetAttributeString("leveltype", "").Equals(overrideCommonness.Key, System.StringComparison.OrdinalIgnoreCase)) { subElement.Attribute("commonness").Value = overrideCommonness.Value.ToString("G", CultureInfo.InvariantCulture); elementFound = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs index 582022214..4944630cc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs @@ -3,10 +3,68 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; +using System.Linq; using Voronoi2; namespace Barotrauma { + class LevelWallVertexBuffer : IDisposable + { + public VertexBuffer WallEdgeBuffer, WallBuffer; + public readonly Texture2D WallTexture, EdgeTexture; + private VertexPositionColorTexture[] wallVertices; + private VertexPositionColorTexture[] wallEdgeVertices; + + public bool IsDisposed + { + get; + private set; + } + + public LevelWallVertexBuffer(VertexPositionTexture[] wallVertices, VertexPositionTexture[] wallEdgeVertices, Texture2D wallTexture, Texture2D edgeTexture, Color color) + { + this.wallVertices = LevelRenderer.GetColoredVertices(wallVertices, color); + WallBuffer = new VertexBuffer(GameMain.Instance.GraphicsDevice, VertexPositionColorTexture.VertexDeclaration, wallVertices.Length, BufferUsage.WriteOnly); + WallBuffer.SetData(this.wallVertices); + WallTexture = wallTexture; + + this.wallEdgeVertices = LevelRenderer.GetColoredVertices(wallEdgeVertices, color); + WallEdgeBuffer = new VertexBuffer(GameMain.Instance.GraphicsDevice, VertexPositionColorTexture.VertexDeclaration, wallEdgeVertices.Length, BufferUsage.WriteOnly); + WallEdgeBuffer.SetData(this.wallEdgeVertices); + EdgeTexture = edgeTexture; + } + + public void Append(VertexPositionTexture[] wallVertices, VertexPositionTexture[] wallEdgeVertices, Color color) + { + WallBuffer.Dispose(); + WallBuffer = new VertexBuffer(GameMain.Instance.GraphicsDevice, VertexPositionColorTexture.VertexDeclaration, this.wallVertices.Length + wallVertices.Length, BufferUsage.WriteOnly); + int originalWallVertexCount = this.wallVertices.Length; + Array.Resize(ref this.wallVertices, originalWallVertexCount + wallVertices.Length); + Array.Copy(LevelRenderer.GetColoredVertices(wallVertices, color), 0, this.wallVertices, originalWallVertexCount, wallVertices.Length); + WallBuffer.SetData(this.wallVertices); + + WallEdgeBuffer.Dispose(); + WallEdgeBuffer = new VertexBuffer(GameMain.Instance.GraphicsDevice, VertexPositionColorTexture.VertexDeclaration, this.wallEdgeVertices.Length + wallEdgeVertices.Length, BufferUsage.WriteOnly); + int originalWallEdgeVertexCount = this.wallEdgeVertices.Length; + Array.Resize(ref this.wallEdgeVertices, originalWallEdgeVertexCount + wallEdgeVertices.Length); + Array.Copy(LevelRenderer.GetColoredVertices(wallEdgeVertices, color), 0, this.wallEdgeVertices, originalWallEdgeVertexCount, wallEdgeVertices.Length); + WallEdgeBuffer.SetData(this.wallEdgeVertices); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + IsDisposed = true; + WallEdgeBuffer?.Dispose(); + WallBuffer?.Dispose(); + } + } + class LevelRenderer : IDisposable { private static BasicEffect wallEdgeEffect, wallCenterEffect; @@ -15,11 +73,11 @@ namespace Barotrauma private Vector2 defaultDustVelocity; private Vector2 dustVelocity; - private RasterizerState cullNone; + private readonly RasterizerState cullNone; - private Level level; + private readonly Level level; - private VertexBuffer wallVertices, bodyVertices; + private readonly List vertexBuffers = new List(); public LevelRenderer(Level level) { @@ -68,6 +126,7 @@ namespace Barotrauma Vector2 currentDustVel = defaultDustVelocity; foreach (LevelObject levelObject in level.LevelObjectManager.GetVisibleObjects()) { + if (levelObject.Triggers == null) { continue; } //use the largest water flow velocity of all the triggers Vector2 objectMaxFlow = Vector2.Zero; foreach (LevelTrigger trigger in levelObject.Triggers) @@ -94,7 +153,6 @@ namespace Barotrauma while (dustOffset.Y <= -waterTextureSize.Y) dustOffset.Y += waterTextureSize.Y; while (dustOffset.Y >= waterTextureSize.Y) dustOffset.Y -= waterTextureSize.Y; } - } public static VertexPositionColorTexture[] GetColoredVertices(VertexPositionTexture[] vertices, Color color) @@ -107,38 +165,27 @@ namespace Barotrauma return verts; } - public void SetWallVertices(VertexPositionTexture[] vertices, Color color) + public void SetVertices(VertexPositionTexture[] wallVertices, VertexPositionTexture[] wallEdgeVertices, Texture2D wallTexture, Texture2D edgeTexture, Color color) { - wallVertices = new VertexBuffer(GameMain.Instance.GraphicsDevice, VertexPositionColorTexture.VertexDeclaration, vertices.Length, BufferUsage.WriteOnly); - wallVertices.SetData(GetColoredVertices(vertices, color)); + var existingBuffer = vertexBuffers.Find(vb => vb.WallTexture == wallTexture && vb.EdgeTexture == edgeTexture); + if (existingBuffer != null) + { + existingBuffer.Append(wallVertices, wallEdgeVertices,color); + } + else + { + vertexBuffers.Add(new LevelWallVertexBuffer(wallVertices, wallEdgeVertices, wallTexture, edgeTexture, color)); + } } - public void SetBodyVertices(VertexPositionTexture[] vertices, Color color) - { - bodyVertices = new VertexBuffer(GameMain.Instance.GraphicsDevice, VertexPositionColorTexture.VertexDeclaration, vertices.Length, BufferUsage.WriteOnly); - bodyVertices.SetData(GetColoredVertices(vertices, color)); - } - - public void SetWallVertices(VertexPositionColorTexture[] vertices) - { - wallVertices = new VertexBuffer(GameMain.Instance.GraphicsDevice, VertexPositionColorTexture.VertexDeclaration, vertices.Length,BufferUsage.WriteOnly); - wallVertices.SetData(vertices); - } - - public void SetBodyVertices(VertexPositionColorTexture[] vertices) - { - bodyVertices = new VertexBuffer(GameMain.Instance.GraphicsDevice, VertexPositionColorTexture.VertexDeclaration, vertices.Length, BufferUsage.WriteOnly); - bodyVertices.SetData(vertices); - } - - public void DrawBackground(SpriteBatch spriteBatch, Camera cam, - LevelObjectManager backgroundSpriteManager = null, + public void DrawBackground(SpriteBatch spriteBatch, Camera cam, + LevelObjectManager backgroundSpriteManager = null, BackgroundCreatureManager backgroundCreatureManager = null) { spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.LinearWrap); Vector2 backgroundPos = cam.WorldViewCenter; - + backgroundPos.Y = -backgroundPos.Y; backgroundPos *= 0.05f; @@ -173,10 +220,13 @@ namespace Barotrauma SamplerState.LinearWrap, DepthStencilState.DepthRead, null, null, cam.Transform); - if (backgroundSpriteManager != null) backgroundSpriteManager.DrawObjects(spriteBatch, cam, drawFront: false); - if (backgroundCreatureManager != null) backgroundCreatureManager.Draw(spriteBatch, cam); + backgroundSpriteManager?.DrawObjects(spriteBatch, cam, drawFront: false); + if (cam.Zoom > 0.05f) + { + backgroundCreatureManager?.Draw(spriteBatch, cam); + } - if (level.GenerationParams.WaterParticles != null) + if (level.GenerationParams.WaterParticles != null && cam.Zoom > 0.05f) { float textureScale = level.GenerationParams.WaterParticleScale; @@ -216,7 +266,7 @@ namespace Barotrauma spriteBatch.End(); - RenderWalls(GameMain.Instance.GraphicsDevice, cam, specular: false); + RenderWalls(GameMain.Instance.GraphicsDevice, cam); spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, @@ -243,7 +293,8 @@ namespace Barotrauma foreach (GraphEdge edge in cell.Edges) { 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)), cell.Body == null ? Color.Cyan * 0.5f : Color.White); + 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); } foreach (Vector2 point in cell.BodyVertices) @@ -252,7 +303,7 @@ namespace Barotrauma } } - foreach (List nodeList in level.SmallTunnels) + /*foreach (List nodeList in level.SmallTunnels) { for (int i = 1; i < nodeList.Count; i++) { @@ -261,7 +312,7 @@ namespace Barotrauma new Vector2(nodeList[i].X, -nodeList[i].Y), Color.Lerp(Color.Yellow, GUI.Style.Red, i / (float)nodeList.Count), 0, 10); } - } + }*/ foreach (var ruin in level.Ruins) { @@ -270,108 +321,142 @@ namespace Barotrauma } Vector2 pos = new Vector2(0.0f, -level.Size.Y); - if (cam.WorldView.Y >= -pos.Y - 1024) { - pos.X = cam.WorldView.X -1024; - int width = (int)(Math.Ceiling(cam.WorldView.Width / 1024 + 4.0f) * 1024); + int topBarrierWidth = level.GenerationParams.WallEdgeSprite.Texture.Width; + int topBarrierHeight = level.GenerationParams.WallEdgeSprite.Texture.Height; - GUI.DrawRectangle(spriteBatch,new Rectangle( - (int)(MathUtils.Round(pos.X, 1024)), - -cam.WorldView.Y, - width, - (int)(cam.WorldView.Y + pos.Y) - 30), + pos.X = cam.WorldView.X - topBarrierWidth; + int width = (int)(Math.Ceiling(cam.WorldView.Width / 1024 + 4.0f) * topBarrierWidth); + + GUI.DrawRectangle(spriteBatch, new Rectangle( + (int)MathUtils.Round(pos.X, topBarrierWidth), + -cam.WorldView.Y, + width, + (int)(cam.WorldView.Y + pos.Y) - 60), Color.Black, true); spriteBatch.Draw(level.GenerationParams.WallEdgeSprite.Texture, - new Rectangle((int)(MathUtils.Round(pos.X, 1024)), (int)pos.Y-1000, width, 1024), - new Rectangle(0, 0, width, -1024), - level.BackgroundTextureColor, 0.0f, + new Rectangle((int)MathUtils.Round(pos.X, topBarrierWidth), (int)(pos.Y - topBarrierHeight + level.GenerationParams.WallEdgeExpandOutwardsAmount), width, topBarrierHeight), + new Rectangle(0, 0, width, -topBarrierHeight), + GameMain.LightManager?.LightingEnabled ?? false ? GameMain.LightManager.AmbientLight : level.WallColor, 0.0f, Vector2.Zero, SpriteEffects.None, 0.0f); } if (cam.WorldView.Y - cam.WorldView.Height < level.SeaFloorTopPos + 1024) { - pos = new Vector2(cam.WorldView.X - 1024, -level.BottomPos); - - int width = (int)(Math.Ceiling(cam.WorldView.Width / 1024 + 4.0f) * 1024); + int bottomBarrierWidth = level.GenerationParams.WallEdgeSprite.Texture.Width; + int bottomBarrierHeight = level.GenerationParams.WallEdgeSprite.Texture.Height; + pos = new Vector2(cam.WorldView.X - bottomBarrierWidth, -level.BottomPos); + int width = (int)(Math.Ceiling(cam.WorldView.Width / bottomBarrierWidth + 4.0f) * bottomBarrierWidth); GUI.DrawRectangle(spriteBatch, new Rectangle( - (int)(MathUtils.Round(pos.X, 1024)), - (int)-(level.BottomPos - 30), - width, - (int)(level.BottomPos - (cam.WorldView.Y - cam.WorldView.Height))), + (int)(MathUtils.Round(pos.X, bottomBarrierWidth)), + -(level.BottomPos - 60), + width, + level.BottomPos - (cam.WorldView.Y - cam.WorldView.Height)), Color.Black, true); spriteBatch.Draw(level.GenerationParams.WallEdgeSprite.Texture, - new Rectangle((int)(MathUtils.Round(pos.X, 1024)), (int)-level.BottomPos, width, 1024), - new Rectangle(0, 0, width, -1024), - level.BackgroundTextureColor, 0.0f, + new Rectangle((int)MathUtils.Round(pos.X, bottomBarrierWidth), -level.BottomPos - (int)level.GenerationParams.WallEdgeExpandOutwardsAmount, width, bottomBarrierHeight), + new Rectangle(0, 0, width, -bottomBarrierHeight), + GameMain.LightManager?.LightingEnabled ?? false ? GameMain.LightManager.AmbientLight : level.WallColor, 0.0f, Vector2.Zero, SpriteEffects.FlipVertically, 0.0f); } } - public void RenderWalls(GraphicsDevice graphicsDevice, Camera cam, bool specular) + public void RenderWalls(GraphicsDevice graphicsDevice, Camera cam) { - if (wallVertices == null) return; + if (!vertexBuffers.Any()) { return; } - bool renderLevel = cam.WorldView.Y >= 0.0f; - bool renderSeaFloor = cam.WorldView.Y - cam.WorldView.Height < level.SeaFloorTopPos + 1024; - - if (!renderLevel && !renderSeaFloor) return; + var defaultRasterizerState = graphicsDevice.RasterizerState; Matrix transformMatrix = cam.ShaderTransform * Matrix.CreateOrthographic(GameMain.GraphicsWidth, GameMain.GraphicsHeight, -1, 100) * 0.5f; - wallEdgeEffect.Texture = specular && level.GenerationParams.WallEdgeSpriteSpecular != null ? - level.GenerationParams.WallEdgeSpriteSpecular.Texture : - level.GenerationParams.WallEdgeSprite.Texture; - wallEdgeEffect.World = transformMatrix; - wallCenterEffect.Texture = specular && level.GenerationParams.WallSpriteSpecular != null ? - level.GenerationParams.WallSpriteSpecular.Texture : - level.GenerationParams.WallSprite.Texture; - wallCenterEffect.World = transformMatrix; - graphicsDevice.SamplerStates[0] = SamplerState.LinearWrap; - wallCenterEffect.CurrentTechnique.Passes[0].Apply(); - - if (renderLevel) - { - graphicsDevice.SetVertexBuffer(bodyVertices); - graphicsDevice.DrawPrimitives(PrimitiveType.TriangleList, 0, (int)Math.Floor(bodyVertices.VertexCount / 3.0f)); - } - - foreach (LevelWall wall in level.ExtraWalls) - { - if (!renderSeaFloor && wall == level.SeaFloor) continue; - wallCenterEffect.World = wall.GetTransform() * transformMatrix; - wallCenterEffect.CurrentTechnique.Passes[0].Apply(); - graphicsDevice.SetVertexBuffer(wall.BodyVertices); - graphicsDevice.DrawPrimitives(PrimitiveType.TriangleList, 0, (int)Math.Floor(wall.BodyVertices.VertexCount / 3.0f)); - } - - var defaultRasterizerState = graphicsDevice.RasterizerState; graphicsDevice.RasterizerState = cullNone; - wallEdgeEffect.World = transformMatrix; - wallEdgeEffect.CurrentTechnique.Passes[0].Apply(); - if (renderLevel) + //render destructible walls + for (int i = 0; i < 2; i++) { - wallEdgeEffect.CurrentTechnique.Passes[0].Apply(); - graphicsDevice.SetVertexBuffer(wallVertices); - graphicsDevice.DrawPrimitives(PrimitiveType.TriangleList, 0, (int)Math.Floor(wallVertices.VertexCount / 3.0f)); + var wallList = i == 0 ? level.ExtraWalls : level.UnsyncedExtraWalls; + foreach (LevelWall wall in wallList) + { + if (!(wall is DestructibleLevelWall destructibleWall) || destructibleWall.Destroyed) { continue; } + + wallCenterEffect.Texture = level.GenerationParams.DestructibleWallSprite?.Texture ?? level.GenerationParams.WallSprite.Texture; + wallCenterEffect.World = wall.GetTransform() * transformMatrix; + wallCenterEffect.Alpha = wall.Alpha; + wallCenterEffect.CurrentTechnique.Passes[0].Apply(); + 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)); + } } - foreach (LevelWall wall in level.ExtraWalls) + + wallEdgeEffect.Alpha = 1.0f; + wallCenterEffect.Alpha = 1.0f; + + wallCenterEffect.World = transformMatrix; + wallEdgeEffect.World = transformMatrix; + + //render static walls + foreach (var vertexBuffer in vertexBuffers) { - if (!renderSeaFloor && wall == level.SeaFloor) continue; - wallEdgeEffect.World = wall.GetTransform() * transformMatrix; + wallCenterEffect.Texture = vertexBuffer.WallTexture; + wallCenterEffect.CurrentTechnique.Passes[0].Apply(); + graphicsDevice.SetVertexBuffer(vertexBuffer.WallBuffer); + graphicsDevice.DrawPrimitives(PrimitiveType.TriangleList, 0, (int)Math.Floor(vertexBuffer.WallBuffer.VertexCount / 3.0f)); + + wallEdgeEffect.Texture = vertexBuffer.EdgeTexture; wallEdgeEffect.CurrentTechnique.Passes[0].Apply(); - graphicsDevice.SetVertexBuffer(wall.WallVertices); - graphicsDevice.DrawPrimitives(PrimitiveType.TriangleList, 0, (int)Math.Floor(wall.WallVertices.VertexCount / 3.0f)); + graphicsDevice.SetVertexBuffer(vertexBuffer.WallEdgeBuffer); + graphicsDevice.DrawPrimitives(PrimitiveType.TriangleList, 0, (int)Math.Floor(vertexBuffer.WallEdgeBuffer.VertexCount / 3.0f)); } + + wallCenterEffect.Texture = level.GenerationParams.WallSprite.Texture; + wallEdgeEffect.Texture = level.GenerationParams.WallEdgeSprite.Texture; + + //render non-destructible extra walls + for (int i = 0; i < 2; i++) + { + var wallList = i == 0 ? level.ExtraWalls : level.UnsyncedExtraWalls; + foreach (LevelWall wall in wallList) + { + if (wall is DestructibleLevelWall) { continue; } + //TODO: use LevelWallVertexBuffers for extra walls as well + wallCenterEffect.World = wall.GetTransform() * transformMatrix; + wallCenterEffect.Alpha = wall.Alpha; + wallCenterEffect.CurrentTechnique.Passes[0].Apply(); + graphicsDevice.SetVertexBuffer(wall.WallBuffer); + graphicsDevice.DrawPrimitives(PrimitiveType.TriangleList, 0, (int)Math.Floor(wall.WallBuffer.VertexCount / 3.0f)); + + 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)); + } + } + graphicsDevice.RasterizerState = defaultRasterizerState; } @@ -383,8 +468,11 @@ namespace Barotrauma protected virtual void Dispose(bool disposing) { - if (wallVertices != null) wallVertices.Dispose(); - if (bodyVertices != null) bodyVertices.Dispose(); + foreach (var vertexBuffer in vertexBuffers) + { + vertexBuffer.Dispose(); + } + vertexBuffers.Clear(); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelWall.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelWall.cs index 7d743bab8..5130227b4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelWall.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelWall.cs @@ -2,55 +2,44 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; +using System.Collections.Generic; namespace Barotrauma { partial class LevelWall : IDisposable { - private VertexBuffer wallVertices, bodyVertices; + public LevelWallVertexBuffer VertexBuffer { get; private set; } - public VertexBuffer WallVertices - { - get { return wallVertices; } - } + public VertexBuffer WallBuffer { get { return VertexBuffer.WallBuffer; } } - public VertexBuffer BodyVertices - { - get { return bodyVertices; } - } + public VertexBuffer WallEdgeBuffer { get { return VertexBuffer.WallEdgeBuffer; } } + + public virtual float Alpha => 1.0f; public Matrix GetTransform() { - return body.BodyType == BodyType.Static ? - Matrix.Identity : - Matrix.CreateRotationZ(body.Rotation) * - Matrix.CreateTranslation(new Vector3(ConvertUnits.ToDisplayUnits(body.Position), 0.0f)); + return Body.FixedRotation ? + Matrix.CreateTranslation(new Vector3(ConvertUnits.ToDisplayUnits(Body.Position), 0.0f)) : + Matrix.CreateRotationZ(Body.Rotation) * + Matrix.CreateTranslation(new Vector3(ConvertUnits.ToDisplayUnits(Body.Position), 0.0f)); } - public void SetWallVertices(VertexPositionTexture[] vertices, Color color) + public void SetWallVertices(VertexPositionTexture[] wallVertices, VertexPositionTexture[] wallEdgeVertices, Texture2D wallTexture, Texture2D edgeTexture, Color color) { - wallVertices = new VertexBuffer(GameMain.Instance.GraphicsDevice, VertexPositionColorTexture.VertexDeclaration, vertices.Length, BufferUsage.WriteOnly); - wallVertices.SetData(LevelRenderer.GetColoredVertices(vertices, color)); + if (VertexBuffer != null && !VertexBuffer.IsDisposed) { VertexBuffer.Dispose(); } + VertexBuffer = new LevelWallVertexBuffer(wallVertices, wallEdgeVertices, wallTexture, edgeTexture, color); } - public void SetBodyVertices(VertexPositionTexture[] vertices, Color color) + public void GenerateVertices() { - bodyVertices = new VertexBuffer(GameMain.Instance.GraphicsDevice, VertexPositionColorTexture.VertexDeclaration, vertices.Length, BufferUsage.WriteOnly); - bodyVertices.SetData(LevelRenderer.GetColoredVertices(vertices, color)); - } - - public void SetWallVertices(VertexPositionColorTexture[] vertices) - { - if (wallVertices != null && !wallVertices.IsDisposed) wallVertices.Dispose(); - wallVertices = new VertexBuffer(GameMain.Instance.GraphicsDevice, VertexPositionColorTexture.VertexDeclaration, vertices.Length, BufferUsage.WriteOnly); - wallVertices.SetData(vertices); - } - - public void SetBodyVertices(VertexPositionColorTexture[] vertices) - { - if (bodyVertices != null && !bodyVertices.IsDisposed) bodyVertices.Dispose(); - bodyVertices = new VertexBuffer(GameMain.Instance.GraphicsDevice, VertexPositionColorTexture.VertexDeclaration, vertices.Length, BufferUsage.WriteOnly); - bodyVertices.SetData(vertices); + float zCoord = this is DestructibleLevelWall ? Rand.Range(0.9f, 1.0f) : 0.9f; + List wallVertices = CaveGenerator.GenerateWallVertices(triangles, level.GenerationParams, zCoord); + SetWallVertices( + wallVertices.ToArray(), + CaveGenerator.GenerateWallEdgeVertices(Cells, level, zCoord).ToArray(), + level.GenerationParams.WallSprite.Texture, + level.GenerationParams.WallEdgeSprite.Texture, + color); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs index 4f863f746..7f207a77e 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) @@ -151,7 +151,16 @@ namespace Barotrauma.Lights private readonly List activeLights = new List(capacity: 100); - public void UpdateLightMap(GraphicsDevice graphics, SpriteBatch spriteBatch, Camera cam, RenderTarget2D backgroundObstructor = null) + public void Update(float deltaTime) + { + foreach (LightSource light in activeLights) + { + if (!light.Enabled) { continue; } + light.Update(deltaTime); + } + } + + public void RenderLightMap(GraphicsDevice graphics, SpriteBatch spriteBatch, Camera cam, RenderTarget2D backgroundObstructor = null) { if (!LightingEnabled) { return; } @@ -203,9 +212,9 @@ 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) { light.DrawSprite(spriteBatch, cam); } + if (light.ParentBody?.UserData is Limb limb && !limb.Hide) { light.DrawSprite(spriteBatch, cam); } } spriteBatch.End(); @@ -215,9 +224,10 @@ namespace Barotrauma.Lights graphics.Clear(AmbientLight); graphics.BlendState = BlendState.Additive; spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Additive, transformMatrix: spriteBatchTransform); + 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); } @@ -262,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(); @@ -327,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); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs index 438cbb3cb..f02802fb9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs @@ -7,7 +7,6 @@ using System.Linq; using System.Text; using System.Xml.Linq; - namespace Barotrauma.Lights { class LightSourceParams : ISerializableEntity @@ -33,7 +32,7 @@ namespace Barotrauma.Lights get { return range; } set { - range = MathHelper.Clamp(value, 0.0f, 2048.0f); + range = MathHelper.Clamp(value, 0.0f, 4096.0f); TextureRange = range; if (OverrideLightTexture != null) { @@ -49,7 +48,63 @@ namespace Barotrauma.Lights [Serialize("0, 0", true), Editable(ValueStep = 1, DecimalCount = 1, MinValueFloat = -1000f, MaxValueFloat = 1000f)] public Vector2 Offset { get; set; } - + + [Serialize(0f, true), Editable(MinValueFloat = -360, MaxValueFloat = 360, ValueStep = 1, DecimalCount = 0)] + public float Rotation { get; set; } + + public Vector2 GetOffset() => Vector2.Transform(Offset, Matrix.CreateRotationZ(Rotation)); + + private float flicker; + [Editable, Serialize(0.0f, false, description: "How heavily the light flickers. 0 = no flickering, 1 = the light will alternate between completely dark and full brightness.")] + public float Flicker + { + get { return flicker; } + set + { + flicker = MathHelper.Clamp(value, 0.0f, 1.0f); + } + } + + [Editable, Serialize(1.0f, false, description: "How fast the light flickers.")] + public float FlickerSpeed + { + get; + set; + } + + private float pulseFrequency; + [Editable, Serialize(0.0f, true, description: "How rapidly the light pulsates (in Hz). 0 = no blinking.")] + public float PulseFrequency + { + get { return pulseFrequency; } + set + { + pulseFrequency = MathHelper.Clamp(value, 0.0f, 60.0f); + } + } + + private float pulseAmount; + [Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f, DecimalCount = 2), Serialize(0.0f, true, description: "How much light pulsates (in Hz). 0 = not at all, 1 = alternates between full brightness and off.")] + public float PulseAmount + { + get { return pulseAmount; } + set + { + pulseAmount = MathHelper.Clamp(value, 0.0f, 1.0f); + } + } + + private float blinkFrequency; + [Editable, Serialize(0.0f, true, description: "How rapidly the light blinks on and off (in Hz). 0 = no blinking.")] + public float BlinkFrequency + { + get { return blinkFrequency; } + set + { + blinkFrequency = MathHelper.Clamp(value, 0.0f, 60.0f); + } + } + public float TextureRange { get; @@ -88,6 +143,7 @@ namespace Barotrauma.Lights switch (subElement.Name.ToString().ToLowerInvariant()) { case "sprite": + case "lightsprite": { LightSprite = new Sprite(subElement); float spriteAlpha = subElement.GetAttributeFloat("alpha", -1.0f); @@ -144,6 +200,8 @@ namespace Barotrauma.Lights private static Texture2D lightTexture; + private float blinkTimer, flickerState, pulseState; + private VertexPositionColorTexture[] vertices; private short[] indices; @@ -296,6 +354,12 @@ namespace Barotrauma.Lights get { return lightSourceParams.Color; } set { lightSourceParams.Color = value; } } + + public float CurrentBrightness + { + get; + private set; + } public float Range { @@ -402,7 +466,33 @@ namespace Barotrauma.Lights texture = LightTexture; diffToSub = new Dictionary(); if (addLight) { GameMain.LightManager.AddLight(this); } + } + 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 && 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 (lightSourceParams.Flicker > 0.0f) + { + flickerState += deltaTime * lightSourceParams.FlickerSpeed; + flickerState %= 255; + brightness *= 1.0f - PerlinNoise.GetPerlin(flickerState, flickerState * 0.5f) * lightSourceParams.Flicker; + } + CurrentBrightness = brightness; } /// @@ -508,13 +598,12 @@ namespace Barotrauma.Lights //recalculate vertices if the subs have moved > 5 px relative to each other Vector2 diff = ParentSub.WorldPosition - sub.WorldPosition; - Vector2 prevDiff; - if (!diffToSub.TryGetValue(sub, out prevDiff)) + if (!diffToSub.TryGetValue(sub, out Vector2 prevDiff)) { diffToSub.Add(sub, diff); NeedsRecalculation = true; } - else if (Vector2.DistanceSquared(diff, prevDiff) > 5.0f*5.0f) + else if (Vector2.DistanceSquared(diff, prevDiff) > 5.0f * 5.0f) { diffToSub[sub] = diff; NeedsRecalculation = true; @@ -884,7 +973,12 @@ namespace Barotrauma.Lights overrideTextureDims = new Vector2(OverrideLightTexture.SourceRect.Width, OverrideLightTexture.SourceRect.Height); Vector2 origin = OverrideLightTextureOrigin; - if (LightSpriteEffect == SpriteEffects.FlipHorizontally) { origin.X = OverrideLightTexture.SourceRect.Width - origin.X; } + if (LightSpriteEffect == SpriteEffects.FlipHorizontally) + { + origin.X = OverrideLightTexture.SourceRect.Width - origin.X; + cosAngle = -cosAngle; + sinAngle = -sinAngle; + } if (LightSpriteEffect == SpriteEffects.FlipVertically) { origin.Y = OverrideLightTexture.SourceRect.Height - origin.Y; } uvOffset = (origin / overrideTextureDims) - new Vector2(0.5f, 0.5f); } @@ -1021,7 +1115,7 @@ namespace Barotrauma.Lights lightVolumeBuffer.SetData(vertices, 0, vertexCount); lightVolumeIndexBuffer.SetData(indices, 0, indexCount); - Vector2 GetUV(Vector2 vert, SpriteEffects effects) + static Vector2 GetUV(Vector2 vert, SpriteEffects effects) { if (effects == SpriteEffects.FlipHorizontally) { @@ -1098,7 +1192,7 @@ namespace Barotrauma.Lights if (DeformableLightSprite != null) { - Vector2 origin = DeformableLightSprite.Origin + LightSourceParams.Offset; + Vector2 origin = DeformableLightSprite.Origin + LightSourceParams.GetOffset(); Vector2 drawPos = position; if (ParentSub != null) { @@ -1115,14 +1209,14 @@ namespace Barotrauma.Lights DeformableLightSprite.Draw( cam, new Vector3(drawPos, 0.0f), - origin, -Rotation, SpriteScale, - new Color(Color, lightSourceParams.OverrideLightSpriteAlpha ?? Color.A / 255.0f), + origin, -Rotation + MathHelper.ToRadians(LightSourceParams.Rotation), SpriteScale, + new Color(Color, (lightSourceParams.OverrideLightSpriteAlpha ?? Color.A / 255.0f) * CurrentBrightness), LightSpriteEffect == SpriteEffects.FlipVertically); } if (LightSprite != null) { - Vector2 origin = LightSprite.Origin + LightSourceParams.Offset; + Vector2 origin = LightSprite.Origin + LightSourceParams.GetOffset(); if ((LightSpriteEffect & SpriteEffects.FlipHorizontally) == SpriteEffects.FlipHorizontally) { origin.X = LightSprite.SourceRect.Width - origin.X; @@ -1140,9 +1234,9 @@ namespace Barotrauma.Lights drawPos.Y = -drawPos.Y; LightSprite.Draw( - spriteBatch, drawPos, - new Color(Color, lightSourceParams.OverrideLightSpriteAlpha ?? Color.A / 255.0f), - origin, -Rotation, SpriteScale, LightSpriteEffect); + spriteBatch, drawPos, + new Color(Color, (lightSourceParams.OverrideLightSpriteAlpha ?? Color.A / 255.0f) * CurrentBrightness), + origin, -Rotation + MathHelper.ToRadians(LightSourceParams.Rotation), SpriteScale, LightSpriteEffect); } if (GameMain.DebugDraw && Screen.Selected.Cam.Zoom > 0.1f) @@ -1185,7 +1279,7 @@ namespace Barotrauma.Lights public void DrawLightVolume(SpriteBatch spriteBatch, BasicEffect lightEffect, Matrix transform) { - if (Range < 1.0f || Color.A < 1) { return; } + if (Range < 1.0f || Color.A < 1 || CurrentBrightness <= 0.0f) { return; } if (CastShadows) { @@ -1207,7 +1301,7 @@ namespace Barotrauma.Lights if (ParentSub != null) drawPos += ParentSub.DrawPosition; drawPos.Y = -drawPos.Y; - spriteBatch.Draw(currentTexture, drawPos, null, Color, -rotation, center, scale, SpriteEffects.None, 1); + spriteBatch.Draw(currentTexture, drawPos, null, Color.Multiply(CurrentBrightness), -rotation, center, scale, SpriteEffects.None, 1); return; } @@ -1229,7 +1323,7 @@ namespace Barotrauma.Lights if (vertexCount == 0) { return; } - lightEffect.DiffuseColor = (new Vector3(Color.R, Color.G, Color.B) * (Color.A / 255.0f)) / 255.0f; + lightEffect.DiffuseColor = (new Vector3(Color.R, Color.G, Color.B) * (Color.A / 255.0f * CurrentBrightness)) / 255.0f; if (OverrideLightTexture != null) { lightEffect.Texture = OverrideLightTexture.Texture; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs index d13f8c34c..d0f7bace4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs @@ -63,6 +63,8 @@ namespace Barotrauma private Sprite[,] mapTiles; private bool[,] tileDiscovered; + private Pair connectionTooltip; + #if DEBUG private GUIComponent editor; @@ -410,6 +412,8 @@ namespace Barotrauma public void Draw(SpriteBatch spriteBatch, GUICustomComponent mapContainer) { + connectionTooltip = null; + Rectangle rect = mapContainer.Rect; Vector2 viewSize = new Vector2(rect.Width / zoom, rect.Height / zoom); @@ -639,6 +643,10 @@ namespace Barotrauma { GUIComponent.DrawToolTip(spriteBatch, tooltip.Second, tooltip.First); } + if (connectionTooltip != null) + { + GUIComponent.DrawToolTip(spriteBatch, connectionTooltip.Second, connectionTooltip.First); + } spriteBatch.End(); GameMain.Instance.GraphicsDevice.ScissorRectangle = prevScissorRect; spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); @@ -689,12 +697,16 @@ namespace Barotrauma int startIndex = connection.CrackSegments.Count > 2 ? 1 : 0; int endIndex = connection.CrackSegments.Count > 2 ? connection.CrackSegments.Count - 1 : connection.CrackSegments.Count; + Vector2? connectionStart = null; + Vector2? connectionEnd = null; for (int i = startIndex; i < endIndex; i++) { var segment = connection.CrackSegments[i]; Vector2 start = rectCenter + (segment[0] + viewOffset) * zoom; - Vector2 end = rectCenter + (segment[1] + viewOffset) * zoom; + if (!connectionStart.HasValue) { connectionStart = start; } + Vector2 end = rectCenter + (segment[1] + viewOffset) * zoom; + connectionEnd = end; if (!viewArea.Contains(start) && !viewArea.Contains(end)) { @@ -734,6 +746,59 @@ namespace Barotrauma connectionSprite.SourceRect, connectionColor * a, MathUtils.VectorToAngle(end - start), new Vector2(0, connectionSprite.size.Y / 2), SpriteEffects.None, 0.01f); } + if (connectionStart.HasValue && connectionEnd.HasValue) + { + GUIComponentStyle crushDepthWarningIconStyle = null; + string tooltip = null; + var subCrushDepth = Submarine.MainSub?.RealWorldCrushDepth ?? Level.DefaultRealWorldCrushDepth; + if (GameMain.GameSession?.Campaign?.UpgradeManager != null) + { + 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); + } + } + } + } + + if (connection.LevelData.InitialDepth * Physics.DisplayToRealWorldRatio > subCrushDepth) + { + crushDepthWarningIconStyle = GUI.Style.GetComponentStyle("CrushDepthWarningHighIcon"); + tooltip = "crushdepthwarninghigh"; + } + else if ((connection.LevelData.InitialDepth + connection.LevelData.Size.Y) * Physics.DisplayToRealWorldRatio > subCrushDepth) + { + crushDepthWarningIconStyle = GUI.Style.GetComponentStyle("CrushDepthWarningLowIcon"); + tooltip = "crushdepthwarninglow"; + } + + 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) + .Replace("[initialdepth]", ((int)(connection.LevelData.InitialDepth * Physics.DisplayToRealWorldRatio)).ToString()) + .Replace("[submarinecrushdepth]", ((int)subCrushDepth).ToString())); + } + } + } if (GameMain.DebugDraw && zoom > 1.0f && generationParams.ShowLevelTypeNames) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs index 67b0c321f..eec523790 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 @@ -329,8 +341,8 @@ namespace Barotrauma { var clones = Clone(selectedList).Where(c => c != null).ToList(); selectedList = clones; - SubEditorScreen.StoreCommand(new AddOrDeleteCommand(clones, false)); selectedList.ForEach(c => c.Move(moveAmount)); + SubEditorScreen.StoreCommand(new AddOrDeleteCommand(clones, false)); } else // move { @@ -897,10 +909,10 @@ namespace Barotrauma { if (entities.Count == 0) { return; } - SubEditorScreen.StoreCommand(new AddOrDeleteCommand(new List(entities), true)); - CopyEntities(entities); - + + SubEditorScreen.StoreCommand(new AddOrDeleteCommand(new List(entities), true)); + entities.ForEach(e => { if (!e.Removed) { e.Remove(); } }); entities.Clear(); } @@ -913,7 +925,6 @@ namespace Barotrauma Clone(copiedList); var clones = mapEntityList.Except(prevEntities).ToList(); - SubEditorScreen.StoreCommand(new AddOrDeleteCommand(clones, false)); var nonWireClones = clones.Where(c => !(c is Item item) || item.GetComponent() == null); if (!nonWireClones.Any()) { nonWireClones = clones; } @@ -929,6 +940,8 @@ namespace Barotrauma clone.Move(moveAmount); clone.Submarine = Submarine.MainSub; } + + SubEditorScreen.StoreCommand(new AddOrDeleteCommand(clones, false, handleInventoryBehavior: false)); } /// diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs index e4c9ee56c..98cfec6fd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -222,9 +222,7 @@ namespace Barotrauma public float GetDrawDepth() { - float depth = SpriteDepthOverrideIsSet ? SpriteOverrideDepth : prefab.sprite.Depth; - depth -= (ID % 255) * 0.000001f; - return depth; + return GetDrawDepth(SpriteDepthOverrideIsSet ? SpriteOverrideDepth : prefab.sprite.Depth, prefab.sprite); } private void Draw(SpriteBatch spriteBatch, bool editing, bool back = true, Effect damageEffect = null) @@ -238,6 +236,7 @@ namespace Barotrauma else if (HiddenInGame) { return; } Color color = IsHighlighted ? GUI.Style.Orange : spriteColor; + if (IsSelected && editing) { //color = Color.Lerp(color, Color.Gold, 0.5f); @@ -253,15 +252,19 @@ namespace Barotrauma thickness: Math.Max(1, (int)(2 / Screen.Selected.Cam.Zoom))); } + bool isWiringMode = editing && SubEditorScreen.TransparentWiringMode && SubEditorScreen.IsWiringMode(); + + if (isWiringMode) { color *= 0.15f; } + Vector2 drawOffset = Submarine == null ? Vector2.Zero : Submarine.DrawPosition; 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) + if (back && damageEffect == null && !isWiringMode) { if (Prefab.BackgroundSprite != null) { @@ -299,7 +302,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) { @@ -324,6 +327,7 @@ namespace Barotrauma 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); @@ -340,21 +344,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, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs index 41a8510e8..795fde68d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs @@ -394,12 +394,10 @@ namespace Barotrauma float expandY = MathHelper.Lerp(30.0f, 0.0f, normalizedDistY); GUI.DrawLine(spriteBatch, - horizontalLine, new Vector2(topLeft.X - expandX, -bottomRight.Y + i * GridSize.Y), new Vector2(bottomRight.X + expandX, -bottomRight.Y + i * GridSize.Y), Color.White * (1.0f - normalizedDistY) * alpha, depth: 0.6f, width: 3); GUI.DrawLine(spriteBatch, - verticalLine, new Vector2(topLeft.X + i * GridSize.X, -topLeft.Y + expandY), new Vector2(topLeft.X + i * GridSize.X, -bottomRight.Y - expandY), Color.White * (1.0f - normalizedDistX) * alpha, depth: 0.6f, width: 3); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineBody.cs index 864b9dd92..e271e9138 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineBody.cs @@ -10,10 +10,7 @@ namespace Barotrauma { if (GameMain.Client == null) { return; } - Vector2 newVelocity = Body.LinearVelocity; - Vector2 newPosition = Body.SimPosition; - - Body.CorrectPosition(positionBuffer, out newPosition, out newVelocity, out _, out _); + Body.CorrectPosition(positionBuffer, out Vector2 newPosition, out Vector2 newVelocity, out _, out _); Vector2 moveAmount = ConvertUnits.ToDisplayUnits(newPosition - Body.SimPosition); newVelocity = newVelocity.ClampLength(100.0f); if (!MathUtils.IsValid(newVelocity) || moveAmount.LengthSquared() < 0.0001f) @@ -24,12 +21,12 @@ namespace Barotrauma List subsToMove = submarine.GetConnectedSubs(); foreach (Submarine dockedSub in subsToMove) { - if (dockedSub == submarine) continue; + if (dockedSub == submarine) { continue; } //clear the position buffer of the docked subs to prevent unnecessary position corrections dockedSub.SubBody.positionBuffer.Clear(); } - Submarine closestSub = null; + Submarine closestSub; if (Character.Controlled == null) { closestSub = Submarine.FindClosest(GameMain.GameScreen.Cam.Position); @@ -63,5 +60,33 @@ namespace Barotrauma if (Character.Controlled != null) Character.Controlled.CursorPosition += moveAmount; } } + + private void PlayDamageSounds(Dictionary damagedStructures, Vector2 impactSimPos, float impact, string soundTag) + { + if (impact < MinCollisionImpact) { return; } + + //play a damage sound for the structure that took the most damage + float maxDamage = 0.0f; + Structure maxDamageStructure = null; + foreach (KeyValuePair structureDamage in damagedStructures) + { + if (maxDamageStructure == null || structureDamage.Value > maxDamage) + { + maxDamage = structureDamage.Value; + maxDamageStructure = structureDamage.Key; + } + } + + if (maxDamageStructure != null) + { + SoundPlayer.PlayDamageSound( + soundTag, + impact * 10.0f, + ConvertUnits.ToDisplayUnits(impactSimPos), + MathHelper.Lerp(2000.0f, 10000.0f, (impact - MinCollisionImpact) / 2.0f), + maxDamageStructure.Tags); + } + } + } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs index d11c022c5..f0cf3371c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs @@ -50,6 +50,8 @@ namespace Barotrauma.Networking Entity targetEntity = Entity.FindEntityByID(msg.ReadUInt16()); int optionIndex = msg.ReadByte(); OrderTarget orderTargetPosition = null; + Order.OrderTargetType orderTargetType = (Order.OrderTargetType)msg.ReadByte(); + int wallSectionIndex = 0; if (msg.ReadBoolean()) { var x = msg.ReadSingle(); @@ -57,6 +59,10 @@ namespace Barotrauma.Networking var hull = Entity.FindEntityByID(msg.ReadUInt16()) as Hull; orderTargetPosition = new OrderTarget(new Vector2(x, y), hull, creatingFromExistingData: true); } + else if(orderTargetType == Order.OrderTargetType.WallSection) + { + wallSectionIndex = msg.ReadByte(); + } Order orderPrefab; if (orderIndex < 0 || orderIndex >= Order.PrefabList.Count) @@ -78,17 +84,31 @@ namespace Barotrauma.Networking if (GameMain.Client.GameStarted && Screen.Selected == GameMain.GameScreen) { - var order = orderTargetPosition == null ? - new Order(orderPrefab, targetEntity, orderPrefab.GetTargetItemComponent(targetEntity as Item), orderGiver: senderCharacter) : - new Order(orderPrefab, orderTargetPosition, orderGiver: senderCharacter); - - if (order.TargetAllCharacters) + Order order = null; + switch (orderTargetType) { - GameMain.GameSession?.CrewManager?.AddOrder(order, orderPrefab.FadeOutTime); + case Order.OrderTargetType.Entity: + order = new Order(orderPrefab, targetEntity, orderPrefab.GetTargetItemComponent(targetEntity as Item), orderGiver: senderCharacter); + break; + case Order.OrderTargetType.Position: + order = new Order(orderPrefab, orderTargetPosition, orderGiver: senderCharacter); + break; + case Order.OrderTargetType.WallSection: + order = new Order(orderPrefab, targetEntity as Structure, wallSectionIndex, orderGiver: senderCharacter); + break; } - else if (targetCharacter != null) + + if (order != null) { - targetCharacter.SetOrder(order, orderOption, senderCharacter); + if (order.TargetAllCharacters) + { + var fadeOutTime = !orderPrefab.IsIgnoreOrder ? (float?)orderPrefab.FadeOutTime : null; + GameMain.GameSession?.CrewManager?.AddOrder(order, fadeOutTime); + } + else if (targetCharacter != null) + { + targetCharacter.SetOrder(order, orderOption, senderCharacter); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChildServerRelay.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChildServerRelay.cs index 4c754132a..7770929fc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChildServerRelay.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChildServerRelay.cs @@ -1,11 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using Barotrauma.IO; +using System.Diagnostics; using System.IO.Pipes; -using System.Text; -using System.Threading; -using System.Threading.Tasks; namespace Barotrauma.Networking { @@ -26,7 +20,15 @@ namespace Barotrauma.Networking PrivateStart(); processInfo.Arguments += " -pipes " + writePipe.GetClientHandleAsString() + " " + readPipe.GetClientHandleAsString(); - Process = Process.Start(processInfo); + try + { + Process = Process.Start(processInfo); + } + catch + { + DebugConsole.ThrowError($"Failed to start ChildServerRelay Process. File: {processInfo.FileName}, arguments: {processInfo.Arguments}"); + throw; + } localHandlesDisposed = false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index df4da0256..dc98b6ac2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -272,6 +272,7 @@ namespace Barotrauma.Networking private void ConnectToServer(object endpoint, string hostName) { LastClientListUpdateID = 0; + foreach (var c in ConnectedClients) { GameMain.NetLobbyScreen.RemovePlayer(c); @@ -361,6 +362,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) @@ -903,6 +912,9 @@ namespace Barotrauma.Networking case ServerPacketHeader.CREW: campaign?.ClientReadCrew(inc); break; + case ServerPacketHeader.READY_CHECK: + ReadyCheck.ClientRead(inc); + break; case ServerPacketHeader.FILE_TRANSFER: fileReceiver.ReadMessage(inc); break; @@ -976,7 +988,7 @@ namespace Barotrauma.Networking string errorMsg = "Level equality check failed. The level generated at your end doesn't match the level generated by the server" + " (client value #" + i + ": " + Level.Loaded.EqualityCheckValues[i] + ", server value #" + i + ": " + levelEqualityCheckValues[i].ToString("X") + - ", level value count: " + levelEqualityCheckValues.Count.ToString("X") + + ", level value count: " + levelEqualityCheckValues.Count + ", seed: " + Level.Loaded.Seed + ", sub: " + Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash.ShortHash + ")" + ", mirrored: " + Level.Loaded.Mirrored + ")."; @@ -2892,8 +2904,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); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/KarmaManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/KarmaManager.cs index b279d2775..71b76babc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/KarmaManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/KarmaManager.cs @@ -61,6 +61,7 @@ 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..2a062c695 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); @@ -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 a6f7279f3..172b124ca 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/OrderChatMessage.cs @@ -13,7 +13,8 @@ namespace Barotrauma.Networking 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)); - if (TargetEntity is OrderTarget orderTarget) + msg.Write((byte)Order.TargetType); + if (Order.TargetType == Order.OrderTargetType.Position && TargetEntity is OrderTarget orderTarget) { msg.Write(true); msg.Write(orderTarget.Position.X); @@ -23,6 +24,10 @@ namespace Barotrauma.Networking else { msg.Write(false); + if (Order.TargetType == Order.OrderTargetType.WallSection) + { + msg.Write((byte)(WallSectionIndex ?? Order.WallSectionIndex ?? 0)); + } } } } 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/SteamManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs index 658879440..21e1faa8a 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; 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/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index b3b0ecc64..eac9ab768 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -372,6 +372,9 @@ namespace Barotrauma.CharacterEditor { base.Update(deltaTime); if (Wizard.instance != null) { return; } + + GameMain.LightManager?.Update((float)deltaTime); + spriteSheetRect = CalculateSpritesheetRectangle(); // Handle shortcut keys if (PlayerInput.KeyHit(Keys.F1)) @@ -787,7 +790,7 @@ namespace Barotrauma.CharacterEditor if (GameMain.LightManager.LightingEnabled) { GameMain.LightManager.ObstructVision = Character.Controlled.ObstructVision; - GameMain.LightManager.UpdateLightMap(graphics, spriteBatch, cam); + GameMain.LightManager.RenderLightMap(graphics, spriteBatch, cam); GameMain.LightManager.UpdateObstructVision(graphics, spriteBatch, cam, Character.Controlled.CursorWorldPosition); } base.Draw(deltaTime, graphics, spriteBatch); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs index b1193a13d..f2b645ef3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs @@ -186,7 +186,7 @@ namespace Barotrauma spriteBatch.End(); graphics.SetRenderTarget(null); - GameMain.LightManager.UpdateLightMap(graphics, spriteBatch, cam, renderTarget); + GameMain.LightManager.RenderLightMap(graphics, spriteBatch, cam, renderTarget); //------------------------------------------------------------------------ graphics.SetRenderTarget(renderTargetBackground); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs index 6eb05728d..c36d6d6d5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs @@ -24,24 +24,26 @@ namespace Barotrauma get { return cam; } } - private GUIFrame leftPanel, rightPanel, bottomPanel, topPanel; + private readonly GUIFrame leftPanel, rightPanel, bottomPanel, topPanel; private LevelGenerationParams selectedParams; private LevelObjectPrefab selectedLevelObject; - private GUIListBox paramsList, ruinParamsList, outpostParamsList, levelObjectList; - private GUIListBox editorContainer; + private readonly GUIListBox paramsList, ruinParamsList, caveParamsList, outpostParamsList, levelObjectList; + private readonly GUIListBox editorContainer; - private GUIButton spriteEditDoneButton; + private readonly GUIButton spriteEditDoneButton; - private GUITextBox seedBox; + private readonly GUITextBox seedBox; - private GUITickBox lightingEnabled, cursorLightEnabled; + private readonly GUITickBox lightingEnabled, cursorLightEnabled; private Sprite editingSprite; private LightSource pointerLightSource; + private readonly Color[] tunnelDebugColors = new Color[] { Color.White, Color.Cyan, Color.LightGreen, Color.Red, Color.LightYellow, Color.LightSeaGreen }; + public LevelEditorScreen() { cam = new Camera() @@ -78,8 +80,17 @@ namespace Barotrauma return true; }; + var caveTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedLeftPanel.RectTransform), TextManager.Get("leveleditor.caveparams"), font: GUI.SubHeadingFont); + + caveParamsList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedLeftPanel.RectTransform)); + caveParamsList.OnSelected += (GUIComponent component, object obj) => + { + CreateCaveParamsEditor(obj as CaveGenerationParams); + return true; + }; + var outpostTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedLeftPanel.RectTransform), TextManager.Get("leveleditor.outpostparams"), font: GUI.SubHeadingFont); - GUITextBlock.AutoScaleAndNormalize(ruinTitle, outpostTitle); + GUITextBlock.AutoScaleAndNormalize(ruinTitle, caveTitle, outpostTitle); outpostParamsList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.2f), paddedLeftPanel.RectTransform)); outpostParamsList.OnSelected += (GUIComponent component, object obj) => @@ -237,13 +248,17 @@ namespace Barotrauma { OnClicked = (btn, obj) => { + bool wasLevelLoaded = Level.Loaded != null; Submarine.Unload(); GameMain.LightManager.ClearLights(); LevelData levelData = LevelData.CreateRandom(seedBox.Text, generationParams: selectedParams); levelData.ForceOutpostGenerationParams = outpostParamsList.SelectedData as OutpostGenerationParams; Level.Generate(levelData, mirror: false); GameMain.LightManager.AddLight(pointerLightSource); - cam.Position = new Vector2(Level.Loaded.Size.X / 2, Level.Loaded.Size.Y / 2); + if (!wasLevelLoaded || cam.Position.X < 0 || cam.Position.Y < 0 || cam.Position.Y > Level.Loaded.Size.X || cam.Position.Y > Level.Loaded.Size.Y) + { + cam.Position = new Vector2(Level.Loaded.Size.X / 2, Level.Loaded.Size.Y / 2); + } foreach (GUITextBlock param in paramsList.Content.Children) { param.TextColor = param.UserData == selectedParams ? GUI.Style.Green : param.Style.TextColor; @@ -344,6 +359,7 @@ namespace Barotrauma editingSprite = null; UpdateParamsList(); UpdateRuinParamsList(); + UpdateCaveParamsList(); UpdateOutpostParamsList(); UpdateLevelObjectsList(); } @@ -371,6 +387,22 @@ namespace Barotrauma } } + private void UpdateCaveParamsList() + { + editorContainer.ClearChildren(); + caveParamsList.Content.ClearChildren(); + + foreach (CaveGenerationParams genParams in CaveGenerationParams.CaveParams) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), caveParamsList.Content.RectTransform) { MinSize = new Point(0, 20) }, + genParams.Name) + { + Padding = Vector4.Zero, + UserData = genParams + }; + } + } + private void UpdateRuinParamsList() { editorContainer.ClearChildren(); @@ -425,6 +457,7 @@ namespace Barotrauma text: ToolBox.LimitString(levelObjPrefab.Name, GUI.SmallFont, paddedFrame.Rect.Width), textAlignment: Alignment.Center, font: GUI.SmallFont) { CanBeFocused = false, + ToolTip = levelObjPrefab.Name }; Sprite sprite = levelObjPrefab.Sprites.FirstOrDefault() ?? levelObjPrefab.DeformableSprite?.Sprite; @@ -437,15 +470,14 @@ namespace Barotrauma } } - private void CreateLevelObjectEditor(LevelObjectPrefab levelObjectPrefab) + private void CreateCaveParamsEditor(CaveGenerationParams caveGenerationParams) { editorContainer.ClearChildren(); - - var editor = new SerializableEntityEditor(editorContainer.Content.RectTransform, levelObjectPrefab, false, true, elementHeight: 20, titleFont: GUI.LargeFont); + var editor = new SerializableEntityEditor(editorContainer.Content.RectTransform, caveGenerationParams, false, true, elementHeight: 20); if (selectedParams != null) { - var commonnessContainer = new GUILayoutGroup(new RectTransform(new Point(editor.Rect.Width, 70)) { IsFixedSize = true }, + var commonnessContainer = new GUILayoutGroup(new RectTransform(new Point(editor.Rect.Width, 70)) { IsFixedSize = true }, isHorizontal: false, childAnchor: Anchor.TopCenter) { AbsoluteSpacing = 5, @@ -457,15 +489,60 @@ namespace Barotrauma { MinValueFloat = 0, MaxValueFloat = 100, - FloatValue = levelObjectPrefab.GetCommonness(selectedParams.Identifier), + FloatValue = caveGenerationParams.GetCommonness(selectedParams), OnValueChanged = (numberInput) => { - levelObjectPrefab.OverrideCommonness[selectedParams.Identifier] = numberInput.FloatValue; + caveGenerationParams.OverrideCommonness[selectedParams.Identifier] = numberInput.FloatValue; } }; new GUIFrame(new RectTransform(new Vector2(1.0f, 0.2f), commonnessContainer.RectTransform), style: null); editor.AddCustomContent(commonnessContainer, 1); } + } + + private void CreateLevelObjectEditor(LevelObjectPrefab levelObjectPrefab) + { + editorContainer.ClearChildren(); + + var editor = new SerializableEntityEditor(editorContainer.Content.RectTransform, levelObjectPrefab, false, true, elementHeight: 20, titleFont: GUI.LargeFont); + + if (selectedParams != null) + { + List availableIdentifiers = new List(); + { + if (selectedParams != null) { availableIdentifiers.Add(selectedParams.Identifier); } + } + foreach (var caveParam in CaveGenerationParams.CaveParams) + { + if (selectedParams != null && caveParam.GetCommonness(selectedParams) <= 0.0f) { continue; } + availableIdentifiers.Add(caveParam.Identifier); + } + availableIdentifiers.Reverse(); + + foreach (string paramsId in availableIdentifiers) + { + var commonnessContainer = new GUILayoutGroup(new RectTransform(new Point(editor.Rect.Width, 70)) { IsFixedSize = true }, + isHorizontal: false, childAnchor: Anchor.TopCenter) + { + AbsoluteSpacing = 5, + Stretch = true + }; + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), commonnessContainer.RectTransform), + TextManager.GetWithVariable("leveleditor.levelobjcommonness", "[leveltype]", paramsId), textAlignment: Alignment.Center); + new GUINumberInput(new RectTransform(new Vector2(0.5f, 0.4f), commonnessContainer.RectTransform), GUINumberInput.NumberType.Float) + { + MinValueFloat = 0, + MaxValueFloat = 100, + FloatValue = selectedParams.Identifier == paramsId ? levelObjectPrefab.GetCommonness(selectedParams) : levelObjectPrefab.GetCommonness(CaveGenerationParams.CaveParams.Find(p => p.Identifier == paramsId)), + OnValueChanged = (numberInput) => + { + levelObjectPrefab.OverrideCommonness[paramsId] = numberInput.FloatValue; + } + }; + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.2f), commonnessContainer.RectTransform), style: null); + editor.AddCustomContent(commonnessContainer, 1); + } + } Sprite sprite = levelObjectPrefab.Sprites.FirstOrDefault() ?? levelObjectPrefab.DeformableSprite?.Sprite; if (sprite != null) @@ -508,7 +585,7 @@ namespace Barotrauma foreach (LevelObjectPrefab objPrefab in LevelObjectPrefab.List) { dropdown.AddItem(objPrefab.Name, objPrefab); - if (childObj.AllowedNames.Contains(objPrefab.Name)) dropdown.SelectItem(objPrefab); + if (childObj.AllowedNames.Contains(objPrefab.Name)) { dropdown.SelectItem(objPrefab); } } dropdown.OnSelected = (selected, obj) => { @@ -590,7 +667,7 @@ namespace Barotrauma foreach (GUIComponent levelObjFrame in levelObjectList.Content.Children) { var levelObj = levelObjFrame.UserData as LevelObjectPrefab; - float commonness = levelObj.GetCommonness(selectedParams.Identifier); + float commonness = levelObj.GetCommonness(selectedParams); levelObjFrame.Color = commonness > 0.0f ? GUI.Style.Green * 0.4f : Color.Transparent; levelObjFrame.SelectedColor = commonness > 0.0f ? GUI.Style.Green * 0.6f : Color.White * 0.5f; levelObjFrame.HoverColor = commonness > 0.0f ? GUI.Style.Green * 0.7f : Color.White * 0.6f; @@ -607,7 +684,7 @@ namespace Barotrauma { var levelObj1 = c1.GUIComponent.UserData as LevelObjectPrefab; var levelObj2 = c2.GUIComponent.UserData as LevelObjectPrefab; - return Math.Sign(levelObj2.GetCommonness(selectedParams.Identifier) - levelObj1.GetCommonness(selectedParams.Identifier)); + return Math.Sign(levelObj2.GetCommonness(selectedParams) - levelObj1.GetCommonness(selectedParams)); }); } @@ -630,7 +707,7 @@ namespace Barotrauma { if (lightingEnabled.Selected) { - GameMain.LightManager.UpdateLightMap(graphics, spriteBatch, cam); + GameMain.LightManager.RenderLightMap(graphics, spriteBatch, cam); } graphics.Clear(Color.Black); @@ -643,6 +720,72 @@ namespace Barotrauma Submarine.DrawFront(spriteBatch); Submarine.DrawDamageable(spriteBatch, null); GUI.DrawRectangle(spriteBatch, new Rectangle(new Point(0, -Level.Loaded.Size.Y), Level.Loaded.Size), Color.Gray, thickness: (int)(1.0f / cam.Zoom)); + + for (int i = 0; i < Level.Loaded.Tunnels.Count; i++) + { + var tunnel = Level.Loaded.Tunnels[i]; + Color tunnelColor = tunnelDebugColors[i % tunnelDebugColors.Length] * 0.2f; + for (int j = 1; j < tunnel.Nodes.Count; j++) + { + Vector2 start = new Vector2(tunnel.Nodes[j - 1].X, -tunnel.Nodes[j - 1].Y); + Vector2 end = new Vector2(tunnel.Nodes[j].X, -tunnel.Nodes[j].Y); + GUI.DrawLine(spriteBatch, start, end, tunnelColor, width: (int)(2.0f / cam.Zoom)); + } + } + + foreach (Level.InterestingPosition interestingPos in Level.Loaded.PositionsOfInterest) + { + if (interestingPos.Position.X < cam.WorldView.X || interestingPos.Position.X > cam.WorldView.Right || + interestingPos.Position.Y > cam.WorldView.Y || interestingPos.Position.Y < cam.WorldView.Y - cam.WorldView.Height) + { + continue; + } + + Vector2 pos = new Vector2(interestingPos.Position.X, -interestingPos.Position.Y); + spriteBatch.DrawCircle(pos, 500, 6, Color.White * 0.5f, thickness: (int)(2 / cam.Zoom)); + GUI.DrawString(spriteBatch, pos, interestingPos.PositionType.ToString(), Color.White, font: GUI.LargeFont); + } + + // TODO: Improve this temporary level editor debug solution (or remove it) + foreach (var pathPoint in Level.Loaded.PathPoints) + { + Vector2 pathPointPos = new Vector2(pathPoint.Position.X, -pathPoint.Position.Y); + foreach (var location in pathPoint.ClusterLocations) + { + if (location.Resources == null) { continue; } + foreach (var resource in location.Resources) + { + Vector2 resourcePos = new Vector2(resource.Position.X, -resource.Position.Y); + spriteBatch.DrawCircle(resourcePos, 100, 6, Color.DarkGreen * 0.5f, thickness: (int)(2 / cam.Zoom)); + GUI.DrawString(spriteBatch, resourcePos, resource.Name, Color.DarkGreen, font: GUI.LargeFont); + var dist = Vector2.Distance(resourcePos, pathPointPos); + var lineStartPos = Vector2.Lerp(resourcePos, pathPointPos, 110 / dist); + var lineEndPos = Vector2.Lerp(pathPointPos, resourcePos, 310 / dist); + GUI.DrawLine(spriteBatch, lineStartPos, lineEndPos, Color.DarkGreen * 0.5f, width: (int)(2 / cam.Zoom)); + } + } + var color = pathPoint.ShouldContainResources ? Color.DarkGreen : Color.DarkRed; + spriteBatch.DrawCircle(pathPointPos, 300, 6, color * 0.5f, thickness: (int)(2 / cam.Zoom)); + GUI.DrawString(spriteBatch, pathPointPos, "Path Point\n" + pathPoint.Id, color, font: GUI.LargeFont); + } + + /*for (int i = 0; i < Level.Loaded.distanceField.Count; i++) + { + GUI.DrawRectangle(spriteBatch, + new Vector2(Level.Loaded.distanceField[i].First.X, -Level.Loaded.distanceField[i].First.Y), + Vector2.One * 5 / cam.Zoom, + ToolBox.GradientLerp((float)(Level.Loaded.distanceField[i].Second / 20000.0), Color.Red, Color.Orange, Color.Yellow, Color.LightGreen), + isFilled: true); + }*/ + /*for (int i = 0; i < Level.Loaded.siteCoordsX.Count; i++) + { + GUI.DrawRectangle(spriteBatch, + new Vector2((float)Level.Loaded.siteCoordsX[i], -(float)Level.Loaded.siteCoordsY[i]), + Vector2.One * 5 / cam.Zoom, + Color.Red, + isFilled: true); + }*/ + spriteBatch.End(); if (lightingEnabled.Selected) @@ -659,12 +802,23 @@ namespace Barotrauma } spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); + if (Level.Loaded != null) + { + float crushDepthScreen = cam.WorldToScreen(new Vector2(0.0f, -Level.Loaded.CrushDepth)).Y; + if (crushDepthScreen > 0.0f && crushDepthScreen < GameMain.GraphicsHeight) + { + 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); + } + } GUI.Draw(Cam, spriteBatch); spriteBatch.End(); } public override void Update(double deltaTime) { + GameMain.LightManager?.Update((float)deltaTime); + pointerLightSource.Position = cam.ScreenToWorld(PlayerInput.MousePosition); pointerLightSource.Enabled = cursorLightEnabled.Selected; pointerLightSource.IsBackground = true; @@ -694,7 +848,40 @@ namespace Barotrauma { foreach (XElement element in doc.Root.Elements()) { - XElement levelParamElement = element; + if (element.IsOverride()) + { + foreach (XElement subElement in element.Elements()) + { + string id = element.GetAttributeString("identifier", null) ?? element.Name.ToString(); + if (!id.Equals(genParams.Name, StringComparison.OrdinalIgnoreCase)) { continue; } + SerializableProperty.SerializeProperties(genParams, element, true); + } + } + else + { + string id = element.GetAttributeString("identifier", null) ?? element.Name.ToString(); + if (!id.Equals(genParams.Name, StringComparison.OrdinalIgnoreCase)) { continue; } + SerializableProperty.SerializeProperties(genParams, element, true); + } + break; + } + } + using (var writer = XmlWriter.Create(configFile.Path, settings)) + { + doc.WriteTo(writer); + writer.Flush(); + } + } + + foreach (ContentFile configFile in GameMain.Instance.GetFilesOfType(ContentType.CaveGenerationParameters)) + { + XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); + if (doc == null) { continue; } + + foreach (CaveGenerationParams genParams in CaveGenerationParams.CaveParams) + { + foreach (XElement element in doc.Root.Elements()) + { if (element.IsOverride()) { foreach (XElement subElement in element.Elements()) @@ -730,7 +917,8 @@ namespace Barotrauma { foreach (XElement element in doc.Root.Elements()) { - if (!element.Name.ToString().Equals(levelObjPrefab.Name, StringComparison.OrdinalIgnoreCase)) { continue; } + string identifier = element.GetAttributeString("identifier", null); + if (!identifier.Equals(levelObjPrefab.Identifier, StringComparison.OrdinalIgnoreCase)) { continue; } levelObjPrefab.Save(element); break; } @@ -846,7 +1034,7 @@ namespace Barotrauma return false; } - if (LevelObjectPrefab.List.Any(obj => obj.Name.ToLower() == nameBox.Text.ToLower())) + if (LevelObjectPrefab.List.Any(obj => obj.Identifier.Equals(nameBox.Text, StringComparison.OrdinalIgnoreCase))) { nameBox.Flash(GUI.Style.Red); GUI.AddMessage(TextManager.Get("leveleditor.levelobjnametaken"), GUI.Style.Red); @@ -860,14 +1048,14 @@ namespace Barotrauma return false; } - newPrefab.Name = nameBox.Text; + newPrefab.Identifier = nameBox.Text; System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings { Indent = true }; foreach (ContentFile configFile in GameMain.Instance.GetFilesOfType(ContentType.LevelObjectPrefabs)) { XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); if (doc == null) { continue; } - var newElement = new XElement(newPrefab.Name); + var newElement = new XElement(newPrefab.Identifier); newPrefab.Save(newElement); newElement.Add(new XElement("Sprite", new XAttribute("texture", texturePathBox.Text), diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index c57281405..b4e064267 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -38,6 +38,9 @@ namespace Barotrauma private GUIImage playstyleBanner; private GUITextBlock playstyleDescription; + private GUIComponent remoteContentContainer; + private XDocument remoteContentDoc; + private Tab selectedTab; private Sprite backgroundSprite; @@ -62,6 +65,14 @@ namespace Barotrauma } CreateHostServerFields(); CreateCampaignSetupUI(); + if (remoteContentDoc?.Root != null) + { + remoteContentContainer.ClearChildren(); + foreach (XElement subElement in remoteContentDoc.Root.Elements()) + { + GUIComponent.FromXML(subElement, remoteContentContainer.RectTransform); + } + } }; new GUIImage(new RectTransform(new Vector2(0.4f, 0.25f), Frame.RectTransform, Anchor.BottomRight) @@ -84,6 +95,11 @@ namespace Barotrauma RelativeSpacing = 0.02f }; + remoteContentContainer = new GUIFrame(new RectTransform(Vector2.One, parent: Frame.RectTransform), style: null) + { + CanBeFocused = false + }; + #if TEST_REMOTE_CONTENT var doc = XMLExtensions.TryLoadXml("Content/UI/MenuTextTest.xml"); @@ -91,7 +107,7 @@ namespace Barotrauma { foreach (XElement subElement in doc?.Root.Elements()) { - GUIComponent.FromXML(subElement, Frame.RectTransform); + GUIComponent.FromXML(subElement, remoteContentContainer.RectTransform); } } #else @@ -1402,10 +1418,10 @@ namespace Barotrauma if (index > 0) { xml = xml.Substring(index, xml.Length - index); } if (!string.IsNullOrWhiteSpace(xml)) { - XElement element = XDocument.Parse(xml)?.Root; - foreach (XElement subElement in element.Elements()) + remoteContentDoc = XDocument.Parse(xml); + foreach (XElement subElement in remoteContentDoc?.Root.Elements()) { - GUIComponent.FromXML(subElement, Frame.RectTransform); + GUIComponent.FromXML(subElement, remoteContentContainer.RectTransform); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index 4f0a13b24..d155c8c03 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -832,7 +832,7 @@ namespace Barotrauma var modeFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.15f), modeList.Content.RectTransform), style: null) { UserData = mode - }; + }; var modeContent = new GUILayoutGroup(new RectTransform(new Vector2(0.75f, 0.9f), modeFrame.RectTransform, Anchor.CenterRight) { RelativeOffset = new Vector2(0.02f, 0.0f) }) { @@ -910,21 +910,24 @@ namespace Barotrauma } }; - missionTypeTickBoxes = new GUITickBox[Enum.GetValues(typeof(MissionType)).Length - 2]; + var missionTypes = (MissionType[])Enum.GetValues(typeof(MissionType)); + missionTypeTickBoxes = new GUITickBox[missionTypes.Length - 2]; int index = 0; - foreach (MissionType missionType in Enum.GetValues(typeof(MissionType))) + for (int i = 0; i < missionTypes.Length; i++) { + var missionType = missionTypes[i]; if (missionType == MissionType.None || missionType == MissionType.All) { continue; } GUIFrame frame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), missionTypeList.Content.RectTransform) { MinSize = new Point(0, (int)(30 * GUI.Scale)) }, style: "ListBoxElement") { - UserData = index, + UserData = missionType, }; missionTypeTickBoxes[index] = new GUITickBox(new RectTransform(Vector2.One, frame.RectTransform), 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; @@ -934,7 +937,6 @@ namespace Barotrauma } }; frame.RectTransform.MinSize = missionTypeTickBoxes[index].RectTransform.MinSize; - index++; } clientDisabledElements.AddRange(missionTypeTickBoxes); @@ -1485,7 +1487,7 @@ namespace Barotrauma return true; } }; - } + } } private void CreateChangesPendingText() @@ -2509,7 +2511,7 @@ namespace Barotrauma Selected = info.Gender == Gender.Female }; - int hairCount = info.FilterByTypeAndHeadID(info.FilterElementsByGenderAndRace(info.Wearables), WearableType.Hair).Count(); + int hairCount = info.FilterByTypeAndHeadID(info.FilterElementsByGenderAndRace(info.Wearables, info.Head.gender, info.Head.race), WearableType.Hair, info.HeadSpriteId).Count(); if (hairCount > 0) { var label = new GUITextBlock(new RectTransform(elementSize, content.RectTransform), TextManager.Get("FaceAttachment.Hair"), font: GUI.SubHeadingFont); @@ -2524,7 +2526,7 @@ namespace Barotrauma }; } - int beardCount = info.FilterByTypeAndHeadID(info.FilterElementsByGenderAndRace(info.Wearables), WearableType.Beard).Count(); + int beardCount = info.FilterByTypeAndHeadID(info.FilterElementsByGenderAndRace(info.Wearables, info.Head.gender, info.Head.race), WearableType.Beard, info.HeadSpriteId).Count(); if (beardCount > 0) { var label = new GUITextBlock(new RectTransform(elementSize, content.RectTransform), TextManager.Get("FaceAttachment.Beard"), font: GUI.SubHeadingFont); @@ -2539,7 +2541,7 @@ namespace Barotrauma }; } - int moustacheCount = info.FilterByTypeAndHeadID(info.FilterElementsByGenderAndRace(info.Wearables), WearableType.Moustache).Count(); + int moustacheCount = info.FilterByTypeAndHeadID(info.FilterElementsByGenderAndRace(info.Wearables, info.Head.gender, info.Head.race), WearableType.Moustache, info.HeadSpriteId).Count(); if (moustacheCount > 0) { var label = new GUITextBlock(new RectTransform(elementSize, content.RectTransform), TextManager.Get("FaceAttachment.Moustache"), font: GUI.SubHeadingFont); @@ -2554,7 +2556,7 @@ namespace Barotrauma }; } - int faceAttachmentCount = info.FilterByTypeAndHeadID(info.FilterElementsByGenderAndRace(info.Wearables), WearableType.FaceAttachment).Count(); + int faceAttachmentCount = info.FilterByTypeAndHeadID(info.FilterElementsByGenderAndRace(info.Wearables, info.Head.gender, info.Head.race), WearableType.FaceAttachment, info.HeadSpriteId).Count(); if (faceAttachmentCount > 0) { var label = new GUITextBlock(new RectTransform(elementSize, content.RectTransform), TextManager.Get("FaceAttachment.Accessories"), font: GUI.SubHeadingFont); @@ -2976,7 +2978,7 @@ namespace Barotrauma public void SelectMode(int modeIndex) { if (modeIndex < 0 || modeIndex >= modeList.Content.CountChildren) { return; } - + if ((GameModePreset)modeList.Content.GetChild(modeIndex).UserData != GameModePreset.MultiPlayerCampaign) { ToggleCampaignMode(false); @@ -3001,16 +3003,33 @@ namespace Barotrauma RefreshGameModeContent(); } + private void RefreshMissionTypes() + { + for (int i = 0; i < missionTypeTickBoxes.Length; i++) + { + MissionType missionType = (MissionType)((int)missionTypeTickBoxes[i].UserData); + if (SelectedMode == GameModePreset.Mission) + { + missionTypeTickBoxes[i].Parent.Visible = MissionPrefab.CoOpMissionClasses.ContainsKey(missionType); + } + else if (SelectedMode == GameModePreset.PvP) + { + missionTypeTickBoxes[i].Parent.Visible = MissionPrefab.PvPMissionClasses.ContainsKey(missionType); + } + } + } + private void RefreshGameModeContent() { if (GameMain.Client == null) { return; } autoRestartBox.Parent.Visible = true; settingsBlocker.Visible = false; - if (SelectedMode == GameModePreset.Mission) + if (SelectedMode == GameModePreset.Mission || SelectedMode == GameModePreset.PvP) { MissionTypeFrame.Visible = true; CampaignFrame.Visible = CampaignSetupFrame.Visible = false; + RefreshMissionTypes(); } else if (SelectedMode == GameModePreset.MultiPlayerCampaign) { @@ -3062,7 +3081,7 @@ namespace Barotrauma RefreshEnabledElements(); if (enabled) { - modeList.Select(2, true); + modeList.Select(GameModePreset.MultiPlayerCampaign, true); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/RoundSummaryScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/RoundSummaryScreen.cs index a75295b1a..5aeea7263 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/RoundSummaryScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/RoundSummaryScreen.cs @@ -61,7 +61,7 @@ namespace Barotrauma { var temp = LoadException; LoadException = null; - throw temp; + System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(temp).Throw(); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs index 73f672fdb..5475c5bc2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs @@ -696,6 +696,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 +717,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) @@ -1061,7 +1072,7 @@ namespace Barotrauma (!filterFull.Selected || serverInfo.PlayerCount < serverInfo.MaxPlayers) && (!filterEmpty.Selected || serverInfo.PlayerCount > 0) && (!filterWhitelisted.Selected || serverInfo.UsingWhiteList == true) && - (filterOffensive.Selected || !ForbiddenWordFilter.IsForbidden(serverInfo.ServerName)) && + (!filterOffensive.Selected || !ForbiddenWordFilter.IsForbidden(serverInfo.ServerName)) && (!filterKarma.Selected || serverInfo.KarmaEnabled == true) && (!filterFriendlyFire.Selected || serverInfo.FriendlyFireEnabled == false) && (!filterTraitor.Selected || serverInfo.TraitorsEnabled == YesNoMaybe.Yes || serverInfo.TraitorsEnabled == YesNoMaybe.Maybe) && diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs index 8e4a46c49..92af24960 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs @@ -353,6 +353,8 @@ namespace Barotrauma { element.Elements("sprite").ForEach(s => CreateSprite(s)); element.Elements("Sprite").ForEach(s => CreateSprite(s)); + element.Elements("deformablesprite").ForEach(s => CreateSprite(s)); + element.Elements("DeformableSprite").ForEach(s => CreateSprite(s)); element.Elements("backgroundsprite").ForEach(s => CreateSprite(s)); element.Elements("BackgroundSprite").ForEach(s => CreateSprite(s)); element.Elements("brokensprite").ForEach(s => CreateSprite(s)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index c5eb94a9d..4d95518c7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -71,6 +71,7 @@ namespace Barotrauma private bool entityMenuOpen = true; private float entityMenuOpenState = 1.0f; + private string lastFilter; public GUIComponent EntityMenu; private GUITextBox entityFilterBox; private GUIListBox entityList; @@ -105,7 +106,25 @@ namespace Barotrauma private static GUIComponent autoSaveLabel; private static int maxAutoSaves = GameSettings.MaximumAutoSaves; - public static bool BulkItemBufferInUse; + public static readonly object ItemAddMutex = new object(), ItemRemoveMutex = new object(); + + public static bool TransparentWiringMode = true; + + private static object bulkItemBufferinUse; + + public static object BulkItemBufferInUse + { + get => bulkItemBufferinUse; + set + { + if (value != bulkItemBufferinUse && bulkItemBufferinUse != null) + { + CommitBulkItemBuffer(); + } + + bulkItemBufferinUse = value; + } + } public static List BulkItemBuffer = new List(); public static List SuppressedWarnings = new List(); @@ -197,7 +216,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 { @@ -630,35 +649,33 @@ namespace Barotrauma return Structure.WallList.Count.ToString(); }; - var lightCountText = new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.0f), paddedEntityCountPanel.RectTransform), TextManager.Get("SubEditorLights"), + var lightCountLabel = new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.0f), paddedEntityCountPanel.RectTransform), TextManager.Get("SubEditorLights"), textAlignment: Alignment.CenterLeft, font: GUI.SmallFont); - var lightCount = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1.0f), lightCountText.RectTransform, Anchor.TopRight, Pivot.TopLeft), "", textAlignment: Alignment.CenterRight); - lightCount.TextGetter = () => + var lightCountText = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1.0f), lightCountLabel.RectTransform, Anchor.TopRight, Pivot.TopLeft), "", textAlignment: Alignment.CenterRight); + lightCountText.TextGetter = () => { - int disabledItemLightCount = 0; + int lightCount = 0; foreach (Item item in Item.ItemList) { - if (item.ParentInventory == null) { continue; } - disabledItemLightCount += item.GetComponents().Count(); + if (item.ParentInventory != null) { continue; } + lightCount += item.GetComponents().Count(); } - int count = GameMain.LightManager.Lights.Count() - disabledItemLightCount; - lightCount.TextColor = ToolBox.GradientLerp(count / 250.0f, GUI.Style.Green, GUI.Style.Orange, GUI.Style.Red); - return count.ToString(); + lightCountText.TextColor = ToolBox.GradientLerp(lightCount / 250.0f, GUI.Style.Green, GUI.Style.Orange, GUI.Style.Red); + return lightCount.ToString(); }; - var shadowCastingLightCountText = new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.0f), paddedEntityCountPanel.RectTransform), TextManager.Get("SubEditorShadowCastingLights"), + var shadowCastingLightCountLabel = new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.0f), paddedEntityCountPanel.RectTransform), TextManager.Get("SubEditorShadowCastingLights"), textAlignment: Alignment.CenterLeft, font: GUI.SmallFont, wrap: true); - var shadowCastingLightCount = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1.0f), shadowCastingLightCountText.RectTransform, Anchor.TopRight, Pivot.TopLeft), "", textAlignment: Alignment.CenterRight); - shadowCastingLightCount.TextGetter = () => + var shadowCastingLightCountText = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1.0f), shadowCastingLightCountLabel.RectTransform, Anchor.TopRight, Pivot.TopLeft), "", textAlignment: Alignment.CenterRight); + shadowCastingLightCountText.TextGetter = () => { - int disabledItemLightCount = 0; + int lightCount = 0; foreach (Item item in Item.ItemList) { - if (item.ParentInventory == null) { continue; } - disabledItemLightCount += item.GetComponents().Count(); + if (item.ParentInventory != null) { continue; } + lightCount += item.GetComponents().Count(l => l.CastShadows); } - int count = GameMain.LightManager.Lights.Count(l => l.CastShadows) - disabledItemLightCount; - shadowCastingLightCount.TextColor = ToolBox.GradientLerp(count / 60.0f, GUI.Style.Green, GUI.Style.Orange, GUI.Style.Red); - return count.ToString(); + shadowCastingLightCountText.TextColor = ToolBox.GradientLerp(lightCount / 60.0f, GUI.Style.Green, GUI.Style.Orange, GUI.Style.Red); + return lightCount.ToString(); }; entityCountPanel.RectTransform.NonScaledSize = new Point( @@ -737,7 +754,13 @@ namespace Barotrauma var filterText = new GUITextBlock(new RectTransform(new Vector2(0.1f, 1.0f), entityMenuTop.RectTransform), TextManager.Get("serverlog.filter"), font: GUI.SubHeadingFont); filterText.RectTransform.MaxSize = new Point((int)(filterText.TextSize.X * 1.5f), int.MaxValue); entityFilterBox = new GUITextBox(new RectTransform(new Vector2(0.17f, 1.0f), entityMenuTop.RectTransform), font: GUI.Font, createClearButton: true); - entityFilterBox.OnTextChanged += (textBox, text) => { FilterEntities(text); return true; }; + entityFilterBox.OnTextChanged += (textBox, text) => + { + if (text == lastFilter) { return true; } + lastFilter = text; + FilterEntities(text); + return true; + }; //spacing new GUIFrame(new RectTransform(new Vector2(0.075f, 1.0f), entityMenuTop.RectTransform), style: null); @@ -994,15 +1017,9 @@ namespace Barotrauma GameMain.LightManager.AmbientLight = Level.Loaded?.GenerationParams?.AmbientLightColor ?? - LevelGenerationParams.LevelParams?.FirstOrDefault()?.AmbientLightColor ?? - new Color(20, 20, 20, 255); + new Color(3, 3, 3, 3); UpdateEntityList(); - if (!wasSelectedBefore) - { - OpenEntityMenu(MapEntityCategory.Structure); - wasSelectedBefore = true; - } isAutoSaving = false; if (!wasSelectedBefore) @@ -1150,7 +1167,7 @@ namespace Barotrauma { if (dummyCharacter != null) RemoveDummyCharacter(); - dummyCharacter = Character.Create(CharacterPrefab.HumanSpeciesName, Vector2.Zero, "", hasAi: false); + dummyCharacter = Character.Create(CharacterPrefab.HumanSpeciesName, Vector2.Zero, "", id: Entity.RespawnManagerID, hasAi: false); dummyCharacter.Info.Name = "Galldren"; //make space for the entity menu @@ -1195,7 +1212,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) @@ -1602,14 +1619,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, @@ -1853,6 +1870,10 @@ namespace Barotrauma Submarine.MainSub.Info.Price = numberInput.IntValue; } }; + if (Submarine.MainSub?.Info != null) + { + Submarine.MainSub.Info.Price = Math.Max(Submarine.MainSub.Info.Price, basePrice); + } if (!Submarine.MainSub.Info.HasTag(SubmarineTag.Shuttle)) { @@ -2665,7 +2686,7 @@ namespace Barotrauma foreach (GUIComponent child in entityList.Content.Children) { - child.Visible = !entityCategory.HasValue || ((MapEntityPrefab) child.UserData).Category == entityCategory; + child.Visible = !entityCategory.HasValue || ((MapEntityPrefab) child.UserData).Category.HasFlag(entityCategory); if (child.Visible && dummyCharacter?.SelectedConstruction?.OwnInventory != null) { child.Visible = child.UserData is MapEntityPrefab item && IsItemPrefab(item); @@ -2790,11 +2811,17 @@ namespace Barotrauma if (PlayerInput.IsShiftDown()) { new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), - TextManager.Get("CharacterEditor.EditBackgroundColor"), font: GUI.SmallFont) + TextManager.Get("SubEditor.EditBackgroundColor"), font: GUI.SmallFont) { UserData = "bgcolor" }; + new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), + TextManager.Get("SubEditor.ToggleTransparency"), font: GUI.SmallFont) + { + UserData = "transparency" + }; + new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), TextManager.Get("editor.selectsame"), font: GUI.SmallFont) { @@ -2870,6 +2897,9 @@ namespace Barotrauma case "bgcolor": CreateBackgroundColorPicker(); break; + case "transparency": + TransparentWiringMode = !TransparentWiringMode; + break; case "selectsame": IEnumerable matching = MapEntity.mapEntityList.Where(e => e.prefab != null && targets.Any(t => t.prefab.Identifier == e.prefab.Identifier) && !MapEntity.SelectedList.Contains(e)); MapEntity.SelectedList.AddRange(matching); @@ -3710,6 +3740,24 @@ namespace Barotrauma } } + private static void CommitBulkItemBuffer() + { + if (BulkItemBuffer.Any()) + { + AddOrDeleteCommand master = BulkItemBuffer[0]; + for (int i = 1; i < BulkItemBuffer.Count; i++) + { + AddOrDeleteCommand command = BulkItemBuffer[i]; + command.MergeInto(master); + } + + StoreCommand(master); + BulkItemBuffer.Clear(); + } + + bulkItemBufferinUse = null; + } + /// /// Allows the game to run logic such as updating the world, /// checking for collisions, gathering input, and playing audio. @@ -3939,6 +3987,11 @@ namespace Barotrauma CloseItem(); } + if (lightingEnabled) + { + GameMain.LightManager?.Update((float)deltaTime); + } + if (contextMenu != null) { Rectangle expandedRect = contextMenu.Rect; @@ -4034,7 +4087,7 @@ namespace Barotrauma // Deposit item from our "infinite stack" into inventory slots var inv = dummyCharacter?.SelectedConstruction?.OwnInventory; - if (inv?.slots != null) + if (inv?.slots != null && !PlayerInput.IsCtrlDown()) { var dragginMouse = MouseDragStart != Vector2.Zero && Vector2.Distance(PlayerInput.MousePosition, MouseDragStart) >= GUI.Scale * 20; @@ -4090,7 +4143,7 @@ namespace Barotrauma if (!newItem.Removed) { - BulkItemBufferInUse = true; + BulkItemBufferInUse = ItemAddMutex; BulkItemBuffer.Add(new AddOrDeleteCommand(new List { newItem }, false)); } @@ -4162,7 +4215,7 @@ namespace Barotrauma List placedEntities = itemInstance.Where(it => !it.Removed).Cast().ToList(); if (placedEntities.Any()) { - BulkItemBufferInUse = true; + BulkItemBufferInUse = ItemAddMutex; BulkItemBuffer.Add(new AddOrDeleteCommand(placedEntities, false)); } } @@ -4174,18 +4227,9 @@ namespace Barotrauma } } - if (BulkItemBufferInUse && PlayerInput.PrimaryMouseButtonReleased() && BulkItemBuffer.Any()) + if (PlayerInput.PrimaryMouseButtonReleased() && BulkItemBufferInUse != null) { - AddOrDeleteCommand master = BulkItemBuffer[0]; - for (int i = 1; i < BulkItemBuffer.Count; i++) - { - AddOrDeleteCommand command = BulkItemBuffer[i]; - command.MergeInto(master); - } - - StoreCommand(master); - BulkItemBuffer.Clear(); - BulkItemBufferInUse = false; + CommitBulkItemBuffer(); } if (SerializableEntityEditor.PropertyChangesActive && (SerializableEntityEditor.NextCommandPush < DateTime.Now || MapEntity.EditingHUD == null)) @@ -4326,7 +4370,7 @@ namespace Barotrauma cam.UpdateTransform(); if (lightingEnabled) { - GameMain.LightManager.UpdateLightMap(graphics, spriteBatch, cam); + GameMain.LightManager.RenderLightMap(graphics, spriteBatch, cam); } foreach (Submarine sub in Submarine.Loaded) @@ -4493,6 +4537,7 @@ namespace Barotrauma stream.Dispose(); } - public static bool IsSubEditor() { return Screen.Selected is SubEditorScreen && !Submarine.Unloading; } + 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 9dca64179..1bd280e4f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -566,8 +566,13 @@ namespace Barotrauma { ToolTip = toolTip }; + + bool isFlagsAttribute = value.GetType().IsDefined(typeof(FlagsAttribute), false); + foreach (object enumValue in Enum.GetValues(value.GetType())) { + if (isFlagsAttribute && !MathHelper.IsPowerOfTwo((int)enumValue)) { continue; } + enumDropDown.AddItem(enumValue.ToString(), enumValue); if (((int)enumValue != 0 || (int)value == 0) && ((Enum)value).HasFlag((Enum)enumValue)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs index e3eb85d5b..8f1594a4e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs @@ -625,7 +625,8 @@ namespace Barotrauma Vector2 soundPos = new Vector2(GameMain.SoundManager.ListenerPosition.X + (fireVolumeRight[i] - fireVolumeLeft[i]) * 100, GameMain.SoundManager.ListenerPosition.Y); if (fireSoundChannels[i] == null || !fireSoundChannels[i].IsPlaying) { - fireSoundChannels[i] = GetSound(fireSoundTags[i]).Play(1.0f, FlowSoundRange, soundPos); + fireSoundChannels[i] = GetSound(fireSoundTags[i])?.Play(1.0f, FlowSoundRange, soundPos); + if (fireSoundChannels[i] == null) { continue; } fireSoundChannels[i].Looping = true; } fireSoundChannels[i].Gain = Math.Max(fireVolumeRight[i], fireVolumeLeft[i]); @@ -960,20 +961,18 @@ namespace Barotrauma if (Level.IsLoadedOutpost && Character.Controlled.Submarine == Level.Loaded.StartOutpost) { - // Only return music type for specific outpost types to not assume that - // every outpost type has an associated music track (switch-case for future tracks) + // Only return music type for location types which have music tracks defined var locationType = Level.Loaded.StartLocation?.Type?.Identifier?.ToLowerInvariant(); - switch (locationType) + if (!string.IsNullOrEmpty(locationType) && musicClips.Any(c => c.Type == locationType)) { - case "research": - return locationType; + return locationType; } } } Submarine targetSubmarine = Character.Controlled?.Submarine; if ((targetSubmarine != null && targetSubmarine.AtDamageDepth) || - (GameMain.GameScreen != null && Screen.Selected == GameMain.GameScreen && GameMain.GameScreen.Cam.Position.Y < SubmarineBody.DamageDepth)) + (GameMain.GameScreen != null && Screen.Selected == GameMain.GameScreen && Level.Loaded != null && Level.Loaded.GetRealWorldDepth(GameMain.GameScreen.Cam.Position.Y) > Level.Loaded.RealWorldCrushDepth)) { return "deep"; } @@ -1026,7 +1025,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/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs index 98a5c6638..e8f122f0c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs @@ -57,65 +57,9 @@ namespace Barotrauma { if (entity == null) { return; } - if (sounds.Count > 0 && playSound) + if (playSound) { - if (soundChannel == null || !soundChannel.IsPlaying) - { - if (soundSelectionMode == SoundSelectionMode.All) - { - foreach (RoundSound sound in sounds) - { - if (sound?.Sound == null) - { - string errorMsg = $"Error in StatusEffect.ApplyProjSpecific1 (sound \"{sound?.Filename ?? "unknown"}\" was null)\n" + Environment.StackTrace.CleanupStackTrace(); - 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); - if (soundChannel != null) { soundChannel.Looping = loopSound; } - } - } - else - { - int selectedSoundIndex; - if (soundSelectionMode == SoundSelectionMode.ItemSpecific && entity is Item item) - { - selectedSoundIndex = item.ID % sounds.Count; - } - else if (soundSelectionMode == SoundSelectionMode.CharacterSpecific && entity is Character user) - { - selectedSoundIndex = user.ID % sounds.Count; - } - else - { - selectedSoundIndex = Rand.Int(sounds.Count); - } - var selectedSound = sounds[selectedSoundIndex]; - if (selectedSound?.Sound == null) - { - string errorMsg = $"Error in StatusEffect.ApplyProjSpecific2 (sound \"{selectedSound?.Filename ?? "unknown"}\" was null)\n" + Environment.StackTrace.CleanupStackTrace(); - GameAnalyticsManager.AddErrorEventOnce("StatusEffect.ApplyProjSpecific:SoundNull2" + Environment.StackTrace.CleanupStackTrace(), GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); - return; - } - if (selectedSound.Sound.Disposed) - { - Submarine.ReloadRoundSound(selectedSound); - } - soundChannel = SoundPlayer.PlaySound(selectedSound.Sound, worldPosition, selectedSound.Volume, selectedSound.Range, hullGuess: hull); - if (soundChannel != null) { soundChannel.Looping = loopSound; } - } - } - else - { - soundChannel.Position = new Vector3(worldPosition, 0.0f); - } - - if (soundChannel != null && soundChannel.Looping) - { - ActiveLoopingSounds.Add(this); - soundEmitter = entity; - loopStartTime = Timing.TotalTime; - } + PlaySound(entity, hull, worldPosition); } foreach (ParticleEmitter emitter in particleEmitters) @@ -151,6 +95,69 @@ namespace Barotrauma } } + private void PlaySound(Entity entity, Hull hull, Vector2 worldPosition) + { + if (sounds.Count == 0) return; + + if (soundChannel == null || !soundChannel.IsPlaying) + { + if (soundSelectionMode == SoundSelectionMode.All) + { + foreach (RoundSound sound in sounds) + { + if (sound?.Sound == null) + { + string errorMsg = $"Error in StatusEffect.ApplyProjSpecific1 (sound \"{sound?.Filename ?? "unknown"}\" was null)\n" + Environment.StackTrace.CleanupStackTrace(); + 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); + if (soundChannel != null) { soundChannel.Looping = loopSound; } + } + } + else + { + int selectedSoundIndex; + if (soundSelectionMode == SoundSelectionMode.ItemSpecific && entity is Item item) + { + selectedSoundIndex = item.ID % sounds.Count; + } + else if (soundSelectionMode == SoundSelectionMode.CharacterSpecific && entity is Character user) + { + selectedSoundIndex = user.ID % sounds.Count; + } + else + { + selectedSoundIndex = Rand.Int(sounds.Count); + } + var selectedSound = sounds[selectedSoundIndex]; + if (selectedSound?.Sound == null) + { + string errorMsg = $"Error in StatusEffect.ApplyProjSpecific2 (sound \"{selectedSound?.Filename ?? "unknown"}\" was null)\n" + Environment.StackTrace.CleanupStackTrace(); + GameAnalyticsManager.AddErrorEventOnce("StatusEffect.ApplyProjSpecific:SoundNull2" + Environment.StackTrace.CleanupStackTrace(), GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + return; + } + if (selectedSound.Sound.Disposed) + { + Submarine.ReloadRoundSound(selectedSound); + } + soundChannel = SoundPlayer.PlaySound(selectedSound.Sound, worldPosition, selectedSound.Volume, selectedSound.Range, hullGuess: hull); + if (soundChannel != null) { soundChannel.Looping = loopSound; } + } + } + else + { + soundChannel.Position = new Vector3(worldPosition, 0.0f); + } + + if (soundChannel != null && soundChannel.Looping) + { + ActiveLoopingSounds.Add(this); + soundEmitter = entity; + loopStartTime = Timing.TotalTime; + } + } + static partial void UpdateAllProjSpecific(float deltaTime) { bool doMuffleCheck = Timing.TotalTime > LastMuffleCheckTime + 0.2; diff --git a/Barotrauma/BarotraumaClient/ClientSource/SubEditorCommands.cs b/Barotrauma/BarotraumaClient/ClientSource/SubEditorCommands.cs index 492b2a8be..df49996e5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/SubEditorCommands.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/SubEditorCommands.cs @@ -120,7 +120,8 @@ namespace Barotrauma /// /// Entities that were deleted or added /// Whether or not all entities are or are going to be deleted - public AddOrDeleteCommand(List receivers, bool wasDeleted) + /// Ignore item inventories when set to false, workaround for pasting + public AddOrDeleteCommand(List receivers, bool wasDeleted, bool handleInventoryBehavior = true) { WasDeleted = wasDeleted; Receivers = receivers; @@ -151,14 +152,17 @@ namespace Barotrauma } } - if (itemsToDelete.Any()) + if (itemsToDelete.Any() && handleInventoryBehavior) { - ContainedItemsCommand.Add(new AddOrDeleteCommand(itemsToDelete, true)); - foreach (MapEntity item in itemsToDelete) + ContainedItemsCommand.Add(new AddOrDeleteCommand(itemsToDelete, wasDeleted)); + if (wasDeleted) { - if (item != null && !item.Removed) + foreach (MapEntity item in itemsToDelete) { - item.Remove(); + if (item != null && !item.Removed) + { + item.Remove(); + } } } } @@ -226,10 +230,10 @@ namespace Barotrauma Debug.Assert(Receivers.All(entity => entity.GetReplacementOrThis().Removed), "Tried to redo a deletion but some items were not deleted"); List clones = MapEntity.Clone(CloneList); - for (int i = 0; i < Math.Min(Receivers.Count, clones.Count); i++) + int length = Math.Min(Receivers.Count, clones.Count); + for (int i = 0; i < length; i++) { - MapEntity clone = clones[i]; - MapEntity receiver = Receivers[i]; + MapEntity clone = clones[i], receiver = Receivers[i]; if (receiver.GetReplacementOrThis() is Item item && clone is Item cloneItem) { @@ -252,6 +256,11 @@ namespace Barotrauma } receiver.GetReplacementOrThis().ReplacedBy = clone; + } + + for (int i = 0; i < length; i++) + { + MapEntity clone = clones[i], receiver = Receivers[i]; if (clone is Item it) { 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 38f1863cc..e6e82b012 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs @@ -150,7 +150,7 @@ namespace Barotrauma return Color.Black; } - if (t <= 0.0f) { return gradient[0]; } + if (t <= 0.0f || !MathUtils.IsValid(t)) { return gradient[0]; } if (t >= 1.0f) { return gradient[gradient.Length - 1]; } float scaledT = t * (gradient.Length - 1); diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index c17176b2c..30c98547c 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.10.6.2 + 0.11.0.9 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 82243b5fd..6b7413f0c 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.10.6.2 + 0.11.0.9 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index bf3a1f2db..ec594cf78 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.10.6.2 + 0.11.0.9 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 8d4fbf757..ef56e6f87 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.10.6.2 + 0.11.0.9 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 1498a636e..8f712c24a 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.10.6.2 + 0.11.0.9 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index 0080258ba..065e27556 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, diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index 3db355286..461837142 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -1708,14 +1708,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); } ); @@ -2155,6 +2156,43 @@ namespace Barotrauma } ); + commands.Add(new Command("readycheck", "Commence a ready check.", (string[] args) => + { + if (Screen.Selected == GameMain.GameScreen && GameMain.NetworkMember != null) + { + CrewManager crewManager = GameMain.GameSession?.CrewManager; + if (crewManager != null && crewManager.ActiveReadyCheck == null) + { + ReadyCheck.StartReadyCheck(""); + NewMessage("Attempted to commence a ready check.", Color.Green); + return; + } + NewMessage("A ready check is already running.", Color.Red); + return; + } + NewMessage("Ready checks cannot be commenced in the lobby.", Color.Red); + })); + + AssignOnClientRequestExecute( + "readycheck", + (senderClient, cursorWorldPos, args) => + { + if (Screen.Selected == GameMain.GameScreen && GameMain.NetworkMember != null && !(GameMain.GameSession?.GameMode?.IsSinglePlayer ?? true)) + { + CrewManager crewManager = GameMain.GameSession?.CrewManager; + if (crewManager != null && crewManager.ActiveReadyCheck == null) + { + ReadyCheck.StartReadyCheck(senderClient.Name, senderClient); + GameMain.Server.SendConsoleMessage("Attempted to commence a ready check.", senderClient); + return; + } + GameMain.Server.SendConsoleMessage("A ready check is already running.", senderClient); + return; + } + GameMain.Server.SendConsoleMessage("Ready checks cannot be commenced in the lobby.", senderClient); + } + ); + #if DEBUG commands.Add(new Command("spamevents", "A debug command that creates a ton of entity events.", (string[] args) => { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/BeaconMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/BeaconMission.cs new file mode 100644 index 000000000..1c590608a --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/BeaconMission.cs @@ -0,0 +1,15 @@ +using Barotrauma.Networking; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Barotrauma +{ + partial class BeaconMission : Mission + { + public override void ServerWriteInitial(IWriteMessage msg, Client c) + { + return; + } + } +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CargoMission.cs index 16de33d5e..e230ab075 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CargoMission.cs @@ -10,7 +10,7 @@ namespace Barotrauma foreach (Item item in items) { item.WriteSpawnData(msg, - item.OriginalID, + item.ID, parentInventoryIDs.ContainsKey(item) ? parentInventoryIDs[item] : Entity.NullEntityID, parentItemContainerIndices.ContainsKey(item) ? parentItemContainerIndices[item] : (byte)0); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MineralMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MineralMission.cs new file mode 100644 index 000000000..354df9c7a --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MineralMission.cs @@ -0,0 +1,31 @@ +using Barotrauma.Networking; + +namespace Barotrauma +{ + partial class MineralMission : Mission + { + public override void ServerWriteInitial(IWriteMessage msg, Client c) + { + foreach (var kvp in SpawnedResources) + { + msg.Write((byte)kvp.Value.Count); + var rotation = ResourceClusters[kvp.Key].Second; + msg.Write(rotation); + foreach (var r in kvp.Value) + { + r.WriteSpawnData(msg, r.ID, Entity.NullEntityID, 0); + } + } + + foreach (var kvp in RelevantLevelResources) + { + msg.Write(kvp.Key); + msg.Write((byte)kvp.Value.Length); + foreach (var i in kvp.Value) + { + msg.Write(i.ID); + } + } + } + } +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MonsterMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MonsterMission.cs index 35bbdcf39..e46a6bf95 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MonsterMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MonsterMission.cs @@ -15,7 +15,7 @@ namespace Barotrauma msg.Write((byte)monsters.Count); foreach (Character monster in monsters) { - monster.WriteSpawnData(msg, monster.OriginalID, restrictMessageSize: false); + monster.WriteSpawnData(msg, monster.ID, restrictMessageSize: false); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/NestMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/NestMission.cs new file mode 100644 index 000000000..17900d793 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/NestMission.cs @@ -0,0 +1,18 @@ +using Barotrauma.Networking; + +namespace Barotrauma +{ + partial class NestMission : Mission + { + public override void ServerWriteInitial(IWriteMessage msg, Client c) + { + msg.Write(nestPosition.X); + msg.Write(nestPosition.Y); + msg.Write((ushort)items.Count); + foreach (Item item in items) + { + item.WriteSpawnData(msg, item.ID, Entity.NullEntityID, 0); + } + } + } +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs index 291414d77..1ba55eee5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs @@ -18,11 +18,11 @@ namespace Barotrauma msg.Write(usedExistingItem); if (usedExistingItem) { - msg.Write(item.OriginalID); + msg.Write(item.ID); } else { - item.WriteSpawnData(msg, item.OriginalID, originalInventoryID, originalItemContainerIndex); + item.WriteSpawnData(msg, item.ID, originalInventoryID, originalItemContainerIndex); } msg.Write((byte)executedEffectIndices.Count); diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs index bb888137a..5cf61396e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -103,6 +103,7 @@ namespace Barotrauma MapEntityPrefab.Init(); MapGenerationParams.Init(); LevelGenerationParams.LoadPresets(); + CaveGenerationParams.LoadPresets(); OutpostGenerationParams.LoadPresets(); EventSet.LoadPrefabs(); Order.Init(); @@ -117,6 +118,7 @@ namespace Barotrauma NPCConversation.LoadAll(GetFilesOfType(ContentType.NPCConversations)); ItemAssemblyPrefab.LoadAll(); LevelObjectPrefab.LoadAll(); + BallastFloraPrefab.LoadAll(GetFilesOfType(ContentType.MapCreature)); GameModePreset.Init(); DecalManager = new DecalManager(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs index 1716e5401..912f8c031 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CharacterCampaignData.cs @@ -30,9 +30,9 @@ namespace Barotrauma return other.SteamID == SteamID && other.ClientEndPoint == ClientEndPoint; } - public void SpawnInventoryItems(CharacterInfo characterInfo, Inventory inventory) + public void SpawnInventoryItems(Character character, Inventory inventory) { - characterInfo.SpawnInventoryItems(inventory, itemData); + character.SpawnInventoryItems(inventory, itemData); } public void ApplyHealthData(CharacterInfo characterInfo, Character character) diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MissionMode.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MissionMode.cs index 81fd38fb4..4d36c08d4 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MissionMode.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MissionMode.cs @@ -1,6 +1,6 @@ namespace Barotrauma { - partial class MissionMode : GameMode + abstract partial class MissionMode : GameMode { public override void ShowStartMessage() { diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index 364f1d075..0171ac8f1 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -216,6 +216,9 @@ namespace Barotrauma characterData.ForEach(cd => cd.HasSpawned = false); + petsElement = new XElement("pets"); + PetBehavior.SavePets(petsElement); + //remove all items that are in someone's inventory foreach (Character c in Character.CharacterList) { @@ -237,9 +240,6 @@ namespace Barotrauma c.Inventory.DeleteAllItems(); } - petsElement = new XElement("pets"); - PetBehavior.SavePets(petsElement); - yield return CoroutineStatus.Running; if (leavingSub != Submarine.MainSub && !leavingSub.DockedTo.Contains(Submarine.MainSub)) diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/ReadyCheck.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/ReadyCheck.cs new file mode 100644 index 000000000..dea506acf --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/ReadyCheck.cs @@ -0,0 +1,119 @@ +#nullable enable +using System.Collections.Generic; +using System.Linq; +using Barotrauma.Networking; + +namespace Barotrauma +{ + internal partial class ReadyCheck + { + private static List ActivePlayers => GameMain.Server.ConnectedClients.Where(c => c != null && !c.Spectating && c.InGame).ToList(); + + public void InitializeReadyCheck(string author, Client? sender = null) + { + foreach (Client client in ActivePlayers) + { + if (client != null && !client.Spectating) + { + IWriteMessage msg = new WriteOnlyMessage(); + msg.Write((byte) ServerPacketHeader.READY_CHECK); + msg.Write((byte) ReadyCheckState.Start); + msg.Write(endTime); + msg.Write(author); + + if (sender != null) + { + msg.Write(true); + msg.Write(sender.ID); + } + else + { + msg.Write(false); + } + + msg.Write((ushort) ActivePlayers.Count); + foreach (byte clientId in Clients.Keys) + { + msg.Write(clientId); + } + + GameMain.Server.ServerPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); + } + } + } + + private void UpdateReadyCheck(byte otherClient, ReadyStatus state) + { + if (Clients.All(pair => pair.Value != ReadyStatus.Unanswered)) + { + EndReadyCheck(); + return; + } + + foreach (Client client in ActivePlayers) + { + IWriteMessage msg = new WriteOnlyMessage(); + msg.Write((byte) ServerPacketHeader.READY_CHECK); + msg.Write((byte) ReadyCheckState.Update); + msg.Write(time); // sync time + msg.Write((byte) state); + msg.Write(otherClient); + GameMain.Server.ServerPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); + } + } + + partial void EndReadyCheck() + { + if (IsFinished) { return; } + IsFinished = true; + foreach (Client client in ActivePlayers) + { + if (client != null && !client.Spectating) + { + IWriteMessage msg = new WriteOnlyMessage(); + msg.Write((byte) ServerPacketHeader.READY_CHECK); + msg.Write((byte) ReadyCheckState.End); + msg.Write((ushort) Clients.Count); + foreach (var (id, state) in Clients) + { + msg.Write(id); + msg.Write((byte) state); + } + + GameMain.Server.ServerPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); + } + } + } + + public static void ServerRead(IReadMessage inc, Client client) + { + ReadyCheckState state = (ReadyCheckState) inc.ReadByte(); + ReadyCheck? readyCheck = GameMain.GameSession?.CrewManager?.ActiveReadyCheck; + + switch (state) + { + case ReadyCheckState.Start when readyCheck == null: + StartReadyCheck(client.Name, client); + break; + case ReadyCheckState.Update when readyCheck != null: + + ReadyStatus status = (ReadyStatus) inc.ReadByte(); + if (!readyCheck.Clients.ContainsKey(client.ID)) { return; } + + readyCheck.Clients[client.ID] = status; + readyCheck.UpdateReadyCheck(client.ID, status); + break; + } + } + + public static void StartReadyCheck(string author, Client? sender = null) + { + if (GameMain.GameSession?.CrewManager == null || GameMain.GameSession.CrewManager.ActiveReadyCheck != null) { return; } + + List connectedClients = GameMain.Server.ConnectedClients; + ReadyCheck newReadyCheck = new ReadyCheck(connectedClients.Where(c => !c.Spectating).Select(c => c.ID).ToList(), 30); + GameMain.GameSession.CrewManager.ActiveReadyCheck = newReadyCheck; + newReadyCheck.InitializeReadyCheck(author, sender); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Door.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Door.cs index 0b988a0dc..afb05dbc6 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Door.cs @@ -31,6 +31,7 @@ namespace Barotrauma.Items.Components msg.Write(isBroken); msg.Write(extraData.Length == 3 ? (bool)extraData[2] : false); //forced open msg.Write(isStuck); + msg.Write(isJammed); msg.WriteRangedSingle(stuck, 0.0f, 100.0f, 8); msg.Write(lastUser == null ? (UInt16)0 : lastUser.ID); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Pump.cs index 1f3c4530d..74ba0b7ce 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Pump.cs @@ -25,7 +25,7 @@ namespace Barotrauma.Items.Components } if (pumpSpeedLockTimer <= 0.0f) { - targetLevel = null; + TargetLevel = null; } FlowPercentage = newFlowPercentage; @@ -41,6 +41,16 @@ namespace Barotrauma.Items.Components //flowpercentage can only be adjusted at 10% intervals -> no need for more accuracy than this 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/Projectile.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs index bc69285de..7d2c4055f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs @@ -5,8 +5,20 @@ namespace Barotrauma.Items.Components { partial class Projectile : ItemComponent { + private float launchRot; + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { + bool launch = extraData.Length > 2 && (bool)extraData[2]; + msg.Write(launch); + if (launch) + { + msg.Write(User.ID); + msg.Write(launchPos.X); + msg.Write(launchPos.Y); + msg.Write(launchRot); + } + bool stuck = StickTarget != null && !item.Removed && !StickTargetRemoved(); msg.Write(stuck); if (stuck) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs index 1950c4adf..4e9fddeb4 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs @@ -1,4 +1,6 @@ using Barotrauma.Networking; +using System.Collections.Generic; +using System.Linq; namespace Barotrauma.Items.Components { @@ -17,15 +19,70 @@ namespace Barotrauma.Items.Components GameServer.Log(GameServer.CharacterLogName(c.Character) + " entered \"" + newOutputValue + "\" on " + item.Name, ServerLog.MessageType.ItemInteraction); OutputValue = newOutputValue; + ShowOnDisplay(newOutputValue); item.SendSignal(0, newOutputValue, "signal_out", null); item.CreateServerEvent(this); } + } + partial void ShowOnDisplay(string input) + { + messageHistory.Add(input); + while (messageHistory.Count > MaxMessages) + { + messageHistory.RemoveAt(0); + } + } + + public void SyncHistory() + { + //split too long messages to multiple parts + foreach (string str in messageHistory) + { + string msgToSend = str; + if (msgToSend.Length > MaxMessageLength) + { + List splitMessage = msgToSend.Split(' ').ToList(); + for (int i = 0; i < splitMessage.Count; i++) + { + if (splitMessage[i].Length > MaxMessageLength) + { + string temp = splitMessage[i]; + splitMessage[i] = temp.Substring(0, MaxMessageLength); + splitMessage.Insert(i + 1, temp.Substring(MaxMessageLength, temp.Length - MaxMessageLength)); + } + } + while (msgToSend.Length > MaxMessageLength) + { + string tempMsg = ""; + do + { + tempMsg += splitMessage[0]; + splitMessage.RemoveAt(0); + if (!splitMessage.Any()) { break; } + tempMsg += " "; + } while (tempMsg.Length + splitMessage[0].Length < MaxMessageLength); + item.CreateServerEvent(this, new string[] { msgToSend }); + msgToSend = msgToSend.Remove(0, tempMsg.Length); + } + } + if (!string.IsNullOrEmpty(msgToSend)) + { + item.CreateServerEvent(this, new string[] { msgToSend }); + } + } } public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { - msg.Write(OutputValue); + if (extraData.Length > 2 && extraData[2] is string str) + { + msg.Write(str); + } + else + { + msg.Write(OutputValue); + } } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs index 2649fbbb4..be554ba94 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs @@ -238,7 +238,7 @@ namespace Barotrauma public void WriteSpawnData(IWriteMessage msg, UInt16 entityID, UInt16 originalInventoryID, byte originalItemContainerIndex) { - if (GameMain.Server == null) return; + if (GameMain.Server == null) { return; } msg.Write(Prefab.OriginalName); msg.Write(Prefab.Identifier); @@ -282,9 +282,16 @@ namespace Barotrauma msg.Write(tagsChanged); if (tagsChanged) { - msg.Write(Tags); + string[] splitTags = Tags.Split(','); + msg.Write(string.Join(',', splitTags.Where(t => !prefab.Tags.Contains(t)))); + msg.Write(string.Join(',', prefab.Tags.Where(t => !splitTags.Contains(t)))); + } + var nameTag = GetComponent(); + msg.Write(nameTag != null); + if (nameTag != null) + { + msg.Write(nameTag.WrittenName ?? ""); } - } partial void UpdateNetPosition(float deltaTime) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs new file mode 100644 index 000000000..2e513b7fa --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs @@ -0,0 +1,81 @@ +using Barotrauma.Items.Components; +using Barotrauma.Networking; +using System; +using System.Xml.Linq; + +namespace Barotrauma.MapCreatures.Behavior +{ + partial class BallastFloraBehavior + { + partial void LoadPrefab(XElement element) + { + foreach (XElement subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "branchsprite": + case "hiddenflowersprite": + break; + case "flowersprite": + flowerVariants++; + break; + case "leafsprite": + leafVariants++; + break; + case "targets": + LoadTargets(subElement); + break; + } + } + } + + + public void ServerWriteSpawn(IWriteMessage msg) + { + msg.Write(Prefab.Identifier); + msg.Write(Offset.X); + msg.Write(Offset.Y); + } + + public void ServerWriteBranchGrowth(IWriteMessage msg, BallastFloraBranch branch, int parentId = -1) + { + var (x, y) = branch.Position; + msg.Write(parentId); + msg.Write((int)branch.ID); + msg.WriteRangedInteger((byte) branch.Type, 0b0000, 0b1111); + msg.WriteRangedInteger((byte) branch.Sides, 0b0000, 0b1111); + msg.WriteRangedInteger(branch.FlowerConfig.Serialize(), 0, 0xFFF); + msg.WriteRangedInteger(branch.LeafConfig.Serialize(), 0, 0xFFF); + msg.Write((ushort) branch.MaxHealth); + msg.Write((int) (x / VineTile.Size)); + msg.Write((int) (y / VineTile.Size)); + } + + public void ServerWriteBranchDamage(IWriteMessage msg, BallastFloraBranch branch, float damage) + { + msg.Write((int)branch.ID); + msg.Write(damage); + msg.Write(branch.Health); + } + + 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) + { + msg.Write(branch.ID); + } + + public void SendNetworkMessage(params object[] extraData) + { + GameMain.Server.CreateEntityEvent(Parent, extraData); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs index 043e85256..68fadc590 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Hull.cs @@ -4,6 +4,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; +using Barotrauma.MapCreatures.Behavior; namespace Barotrauma { @@ -70,6 +71,39 @@ namespace Barotrauma public void ServerWrite(IWriteMessage message, Client c, object[] extraData = null) { + if (extraData != null && extraData.Length >= 2 && extraData[0] is BallastFloraBehavior behavior && extraData[1] is BallastFloraBehavior.NetworkHeader header) + { + message.Write(true); + message.Write((byte)header); + + switch (header) + { + case BallastFloraBehavior.NetworkHeader.Spawn: + behavior.ServerWriteSpawn(message); + break; + case BallastFloraBehavior.NetworkHeader.Kill: + break; + case BallastFloraBehavior.NetworkHeader.BranchCreate when extraData.Length >= 4 && extraData[2] is BallastFloraBranch branch && extraData[3] is int parentId: + behavior.ServerWriteBranchGrowth(message, branch, parentId); + break; + case BallastFloraBehavior.NetworkHeader.BranchDamage when extraData.Length >= 4 && extraData[2] is BallastFloraBranch branch && extraData[3] is float damage: + behavior.ServerWriteBranchDamage(message, branch, damage); + break; + case BallastFloraBehavior.NetworkHeader.BranchRemove when extraData.Length >= 3 && extraData[2] is BallastFloraBranch branch: + behavior.ServerWriteBranchRemove(message, branch); + break; + case BallastFloraBehavior.NetworkHeader.Infect when extraData.Length >= 4 && extraData[2] is UInt16 itemID && extraData[3] is bool infect: + BallastFloraBranch infector = null; + if (extraData.Length >= 5 && extraData[4] is BallastFloraBranch b) { infector = b; } + behavior.ServerWriteInfect(message, itemID, infect, infector); + break; + } + + message.Write(behavior.PowerConsumptionTimer); + return; + } + + message.Write(false); message.WriteRangedSingle(MathHelper.Clamp(waterVolume / Volume, 0.0f, 1.5f), 0.0f, 1.5f, 8); message.WriteRangedSingle(MathHelper.Clamp(OxygenPercentage, 0.0f, 100.0f), 0.0f, 100.0f, 8); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs index 505dd4621..83b790157 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs @@ -127,8 +127,7 @@ namespace Barotrauma.Networking if (IPAddress.IsLoopback(IP)) { return false; } var bannedPlayer = bannedPlayers.Find(bp => bp.CompareTo(IP) || - (steamID > 0 && bp.SteamID == steamID) || - (SteamManager.SteamIDStringToUInt64(bp.EndPoint) == steamID)); + (steamID > 0 && (bp.SteamID == steamID || SteamManager.SteamIDStringToUInt64(bp.EndPoint) == steamID))); reason = bannedPlayer?.Reason; return bannedPlayer != null; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs index a8135919f..d6ea2bad9 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs @@ -18,12 +18,15 @@ namespace Barotrauma.Networking Entity orderTargetEntity = null; OrderChatMessage orderMsg = null; OrderTarget orderTargetPosition = null; + Order.OrderTargetType orderTargetType = Order.OrderTargetType.Entity; + int? wallSectionIndex = null; if (type == ChatMessageType.Order) { int orderIndex = msg.ReadByte(); orderTargetCharacter = Entity.FindEntityByID(msg.ReadUInt16()) as Character; orderTargetEntity = Entity.FindEntityByID(msg.ReadUInt16()) as Entity; int orderOptionIndex = msg.ReadByte(); + orderTargetType = (Order.OrderTargetType)msg.ReadByte(); if (msg.ReadBoolean()) { var x = msg.ReadSingle(); @@ -31,6 +34,10 @@ namespace Barotrauma.Networking var hull = Entity.FindEntityByID(msg.ReadUInt16()) as Hull; orderTargetPosition = new OrderTarget(new Vector2(x, y), hull, true); } + else if (orderTargetType == Order.OrderTargetType.WallSection) + { + wallSectionIndex = msg.ReadByte(); + } if (orderIndex < 0 || orderIndex >= Order.PrefabList.Count) { @@ -39,9 +46,12 @@ namespace Barotrauma.Networking return; } - Order order = Order.PrefabList[orderIndex]; - string orderOption = orderOptionIndex < 0 || orderOptionIndex >= order.Options.Length ? "" : order.Options[orderOptionIndex]; - orderMsg = new OrderChatMessage(order, orderOption, orderTargetPosition ?? orderTargetEntity as ISpatialEntity, orderTargetCharacter, c.Character); + 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) + { + WallSectionIndex = wallSectionIndex + }; txt = orderMsg.Text; } else @@ -119,16 +129,39 @@ namespace Barotrauma.Networking if (type == ChatMessageType.Order) { if (c.Character == null || c.Character.SpeechImpediment >= 100.0f || c.Character.IsDead) { return; } - if (orderMsg.Order.TargetAllCharacters) + Order order = null; + if (orderMsg.Order.IsReport) { HumanAIController.ReportProblem(orderMsg.Sender, orderMsg.Order); } - else if (orderTargetCharacter != null) + else if (orderTargetCharacter != null && !orderMsg.Order.TargetAllCharacters) { - var order = orderTargetPosition == null ? - new Order(orderMsg.Order.Prefab, orderTargetEntity, orderMsg.Order.Prefab?.GetTargetItemComponent(orderTargetEntity as Item), orderMsg.Sender) : - new Order(orderMsg.Order.Prefab, orderTargetPosition, orderMsg.Sender); - orderTargetCharacter.SetOrder(order, orderMsg.OrderOption, orderMsg.Sender); + switch (orderTargetType) + { + case Order.OrderTargetType.Entity: + order = new Order(orderMsg.Order.Prefab, orderTargetEntity, orderMsg.Order.Prefab?.GetTargetItemComponent(orderTargetEntity as Item), orderGiver: orderMsg.Sender); + break; + case Order.OrderTargetType.Position: + order = new Order(orderMsg.Order.Prefab, orderTargetPosition, orderGiver: orderMsg.Sender); + break; + } + if (order != null) + { + orderTargetCharacter.SetOrder(order, orderMsg.OrderOption, orderMsg.Sender); + } + } + else if (orderMsg.Order.IsIgnoreOrder) + { + switch (orderTargetType) + { + case Order.OrderTargetType.Entity: + (orderTargetEntity as MapEntity)?.SetIgnoreByAI(orderMsg.Order.Identifier == "ignorethis"); + break; + case Order.OrderTargetType.WallSection: + if (!wallSectionIndex.HasValue) { break; } + (orderTargetEntity as Structure)?.GetSection(wallSectionIndex.Value)?.SetIgnoreByAI(orderMsg.Order.Identifier == "ignorethis"); + break; + } } GameMain.Server.SendOrderChatMessage(orderMsg); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index b607fd2b9..c8d387363 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -811,6 +811,9 @@ namespace Barotrauma.Networking case ClientPacketHeader.CREW: ReadCrewMessage(inc, connectedClient); break; + case ClientPacketHeader.READY_CHECK: + ReadyCheck.ServerRead(inc, connectedClient); + break; case ClientPacketHeader.FILE_REQUEST: if (serverSettings.AllowFileTransfers) { @@ -1543,10 +1546,12 @@ 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 >= NetConfig.DisableCharacterDistSqr) { continue; } } else { @@ -2130,6 +2135,7 @@ namespace Barotrauma.Networking Level.Loaded?.SpawnNPCs(); Level.Loaded?.SpawnCorpses(); + Level.Loaded?.PrepareBeaconStation(); AutoItemPlacer.PlaceIfNeeded(); CrewManager crewManager = campaign?.CrewManager; @@ -2253,7 +2259,7 @@ namespace Barotrauma.Networking for (int i = 0; i < teamClients.Count; i++) { - Character spawnedCharacter = Character.Create(teamClients[i].CharacterInfo, spawnWaypoints[i].WorldPosition, teamClients[i].CharacterInfo.Name, true, false); + Character spawnedCharacter = Character.Create(teamClients[i].CharacterInfo, spawnWaypoints[i].WorldPosition, teamClients[i].CharacterInfo.Name, isRemotePlayer: true, hasAi: false); spawnedCharacter.AnimController.Frozen = true; spawnedCharacter.TeamID = teamID; teamClients[i].Character = spawnedCharacter; @@ -2269,7 +2275,7 @@ namespace Barotrauma.Networking } else { - characterData.SpawnInventoryItems(spawnedCharacter.Info, spawnedCharacter.Inventory); + characterData.SpawnInventoryItems(spawnedCharacter, spawnedCharacter.Inventory); characterData.ApplyHealthData(spawnedCharacter.Info, spawnedCharacter); spawnedCharacter.GiveIdCardTags(mainSubWaypoints[i]); characterData.HasSpawned = true; @@ -2280,7 +2286,7 @@ namespace Barotrauma.Networking for (int i = teamClients.Count; i < teamClients.Count + bots.Count; i++) { - Character spawnedCharacter = Character.Create(characterInfos[i], spawnWaypoints[i].WorldPosition, characterInfos[i].Name, false, true); + Character spawnedCharacter = Character.Create(characterInfos[i], spawnWaypoints[i].WorldPosition, characterInfos[i].Name, isRemotePlayer: false, hasAi: true); spawnedCharacter.TeamID = teamID; spawnedCharacter.GiveJobItems(mainSubWaypoints[i]); spawnedCharacter.GiveIdCardTags(mainSubWaypoints[i]); @@ -2945,7 +2951,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 @@ -3662,7 +3668,7 @@ namespace Barotrauma.Networking public static void Log(string line, ServerLog.MessageType messageType) { - if (GameMain.Server == null || !GameMain.Server.ServerSettings.SaveServerLogs) return; + if (GameMain.Server == null || !GameMain.Server.ServerSettings.SaveServerLogs) { return; } GameMain.Server.ServerSettings.ServerLog.WriteLine(line, messageType); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs index 1ecbbab59..54969054e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs @@ -226,6 +226,13 @@ namespace Barotrauma clientMemories.Remove(client); } + public void OnBallastFloraDamaged(Character character, float damage) + { + if (character == null) { return; } + float karmaChange = damage * BallastFloraKarmaIncrease; + AdjustKarma(character, karmaChange, "Damaged ballast flora"); + } + // ReSharper disable once UseNegatedPatternMatching, LoopCanBeConvertedToQuery public void OnItemTakenFromPlayer(CharacterInventory inventory, Client yoinker, Item item) { 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 eaf7012a1..dd80754d3 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/OrderChatMessage.cs @@ -21,7 +21,8 @@ namespace Barotrauma.Networking 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)); - if (TargetEntity is OrderTarget orderTarget) + msg.Write((byte)Order.TargetType); + if (Order.TargetType == Order.OrderTargetType.Position && TargetEntity is OrderTarget orderTarget) { msg.Write(true); msg.Write(orderTarget.Position.X); @@ -31,6 +32,10 @@ namespace Barotrauma.Networking else { msg.Write(false); + if (Order.TargetType == Order.OrderTargetType.WallSection) + { + msg.Write((byte)(WallSectionIndex ?? Order.WallSectionIndex ?? 0)); + } } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs index d8674f03d..85ce04c9a 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; @@ -224,7 +233,7 @@ namespace Barotrauma.Networking case ConnectionInitialization.ContentPackageOrder: outMsg.Write(GameMain.Server.ServerName); - var mpContentPackages = GameMain.Config.AllEnabledPackages.Where(cp => cp.HasMultiplayerIncompatibleContent).ToList(); + var mpContentPackages = GameMain.Config.AllEnabledPackages.Where(cp => cp.HasMultiplayerIncompatibleContent).Reverse().ToList(); outMsg.WriteVariableUInt32((UInt32)mpContentPackages.Count); for (int i = 0; i < mpContentPackages.Count; i++) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index dcbeafe5b..be0d193bd 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -275,7 +275,7 @@ namespace Barotrauma.Networking characterInfos[i].CurrentOrder = null; characterInfos[i].CurrentOrderOption = null; - var character = Character.Create(characterInfos[i], shuttleSpawnPoints[i].WorldPosition, characterInfos[i].Name, !bot, bot); + var character = Character.Create(characterInfos[i], shuttleSpawnPoints[i].WorldPosition, characterInfos[i].Name, isRemotePlayer: !bot, hasAi: bot); character.TeamID = Character.TeamType.Team1; if (bot) @@ -341,7 +341,7 @@ namespace Barotrauma.Networking } else { - characterData.SpawnInventoryItems(character.Info, character.Inventory); + characterData.SpawnInventoryItems(character, character.Inventory); characterData.ApplyHealthData(character.Info, character); character.GiveIdCardTags(mainSubSpawnPoints[i]); characterData.HasSpawned = true; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Program.cs b/Barotrauma/BarotraumaServer/ServerSource/Program.cs index 2705bd8fa..5be940ee3 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Program.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Program.cs @@ -123,9 +123,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 +137,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/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 7aa69fd27..08cc5c17b 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.10.6.2 + 0.11.0.9 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml index ba7147768..0fe12a622 100644 --- a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml +++ b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml @@ -99,6 +99,8 @@ + + @@ -143,7 +145,7 @@ - + @@ -151,7 +153,30 @@ - + + + + + + + + + + + + + + + + + + + + + + + + @@ -209,4 +234,5 @@ + \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/Data/forbiddenwordlist.txt b/Barotrauma/BarotraumaShared/Data/forbiddenwordlist.txt index 10b9a44d3..7e3c1430c 100644 --- a/Barotrauma/BarotraumaShared/Data/forbiddenwordlist.txt +++ b/Barotrauma/BarotraumaShared/Data/forbiddenwordlist.txt @@ -1,32 +1,57 @@ adolf anal anus +anuses ass +asses bitch +bitches blowjob +blowjobs clitoris cock +cocks cunt +cunts dick +dicks dildo +dildos dyke +dykes gay +gays fag +fags faggot +faggots fuck hitler homo +homos jew +jews kike +kikes nazi +nazis mudslime +mudslimes nig nigger +niggers nigga +niggas penis pussy +pussies queer +queers slut +sluts twat +twats vagina -whore \ No newline at end of file +vaginas +whore +whores \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/Data/karmasettings.xml b/Barotrauma/BarotraumaShared/Data/karmasettings.xml index f42b02a73..4ab2df218 100644 --- a/Barotrauma/BarotraumaShared/Data/karmasettings.xml +++ b/Barotrauma/BarotraumaShared/Data/karmasettings.xml @@ -24,7 +24,8 @@ karmanotificationinterval="15" resetkarmabetweenrounds="true" dangerousitemstealkarmadecrease="15" - dangerousitemstealbots="false" /> + dangerousitemstealbots="false" + ballastflorakarmaincrease="0.05" /> + dangerousitemstealbots="true" + ballastflorakarmaincrease="0.03" /> + dangerousitemstealbots="false" + ballastflorakarmaincrease="0.05" /> \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/Data/permissionpresets.xml b/Barotrauma/BarotraumaShared/Data/permissionpresets.xml index 8b60010c0..0bad02e51 100644 --- a/Barotrauma/BarotraumaShared/Data/permissionpresets.xml +++ b/Barotrauma/BarotraumaShared/Data/permissionpresets.xml @@ -10,6 +10,7 @@ description="Allowed to manage round settings, kick players and access server logs." permissions="ManageRound,Kick,SelectSub,SelectMode,ManageCampaign,ConsoleCommands,ServerLog"> + @@ -39,6 +40,7 @@ description="Allowed to ban and kick players, manage round settings, access server logs and use nearly all console commands." permissions="All"> + diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 80282e08a..bcb3b4ebe 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -91,9 +91,12 @@ namespace Barotrauma private float avoidTimer; private float observeTimer; + private float sweepTimer; public bool StayInsideLevel = true; + private readonly IEnumerable myBodies; + public LatchOntoAI LatchOntoAI { get; private set; } public SwarmBehavior SwarmBehavior { get; private set; } public PetBehavior PetBehavior { get; private set; } @@ -195,6 +198,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()) { @@ -223,6 +239,7 @@ namespace Barotrauma requiredHoleCount = (int)Math.Ceiling(ConvertUnits.ToDisplayUnits(colliderWidth) / Structure.WallSectionSize); avoidLookAheadDistance = Math.Max(colliderWidth * 3, 1.5f); + myBodies = Character.AnimController.Limbs.Select(l => l.body.FarseerBody); } public CharacterParams.AIParams AIParams => Character.Params.AI; @@ -434,7 +451,7 @@ namespace Barotrauma UpdateIdle(deltaTime); break; case AIState.Attack: - run = !IsCoolDownRunning; + run = !IsCoolDownRunning || AttackingLimb != null && AttackingLimb.attack.FullSpeedAfterAttack; UpdateAttack(deltaTime); break; case AIState.Eat: @@ -471,7 +488,7 @@ namespace Barotrauma { bool isBeingChased = IsBeingChased; float reactDistance = !isBeingChased && selectedTargetingParams != null && selectedTargetingParams.ReactDistance > 0 ? selectedTargetingParams.ReactDistance : GetPerceivingRange(SelectedAiTarget); - if (squaredDistance <= Math.Pow(reactDistance + movementMargin, 2)) + if (squaredDistance <= Math.Pow(reactDistance, 2)) { float halfReactDistance = reactDistance / 2; float attackDistance = selectedTargetingParams != null && selectedTargetingParams.AttackDistance > 0 ? selectedTargetingParams.AttackDistance : halfReactDistance; @@ -483,17 +500,12 @@ namespace Barotrauma else { run = isBeingChased ? true : squaredDistance < Math.Pow(halfReactDistance, 2); - if (movementMargin <= 0) - { - movementMargin = halfReactDistance; - } - movementMargin = MathHelper.Clamp(movementMargin += deltaTime, halfReactDistance, reactDistance); - UpdateEscape(deltaTime); + State = AIState.Escape; + avoidTimer = AIParams.AvoidTime * 0.5f * Rand.Range(0.75f, 1.25f); } } else { - movementMargin = 0; UpdateIdle(deltaTime); } } @@ -617,7 +629,7 @@ namespace Barotrauma #region Idle - private void UpdateIdle(float deltaTime) + private void UpdateIdle(float deltaTime, bool followLastTarget = true) { var pathSteering = SteeringManager as IndoorsSteeringManager; if (pathSteering == null) @@ -630,32 +642,35 @@ namespace Barotrauma return; } } - var target = SelectedAiTarget ?? _lastAiTarget; - if (target?.Entity != null && !target.Entity.Removed && PreviousState == AIState.Attack && Character.CurrentHull == null) + if (followLastTarget) { - // Keep heading to the last known position of the target - var memory = GetTargetMemory(target, false); - if (memory != null) + var target = SelectedAiTarget ?? _lastAiTarget; + if (target?.Entity != null && !target.Entity.Removed && PreviousState == AIState.Attack && Character.CurrentHull == null) { - var location = memory.Location; - float dist = Vector2.DistanceSquared(WorldPosition, location); - if (dist < 50 * 50) + // Keep heading to the last known position of the target + var memory = GetTargetMemory(target, false); + if (memory != null) { - // Target is gone - ResetAITarget(); + var location = memory.Location; + float dist = Vector2.DistanceSquared(WorldPosition, location); + if (dist < 50 * 50) + { + // Target is gone + ResetAITarget(); + } + else + { + // Steer towards the target + SteeringManager.SteeringSeek(Character.GetRelativeSimPosition(target.Entity, location), 5); + SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15); + return; + } } else { - // Steer towards the target - SteeringManager.SteeringSeek(Character.GetRelativeSimPosition(target.Entity, location), 5); - SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15); - return; + ResetAITarget(); } } - else - { - ResetAITarget(); - } } if (pathSteering != null && !Character.AnimController.InWater) { @@ -686,7 +701,7 @@ namespace Barotrauma State = AIState.Idle; return; } - else if (selectedTargetMemory != null) + else if (selectedTargetMemory != null && SelectedAiTarget?.Entity is Character) { selectedTargetMemory.Priority += deltaTime * priorityFearIncreasement; } @@ -880,7 +895,7 @@ namespace Barotrauma { if (door.LinkedGap.Size > ConvertUnits.ToDisplayUnits(colliderWidth)) { - LatchOntoAI?.DeattachFromBody(cooldown: 2); + LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 2); Character.AnimController.ReleaseStuckLimbs(); var velocity = Vector2.Normalize(door.LinkedGap.FlowTargetHull.WorldPosition - Character.WorldPosition); steeringManager.SteeringManual(deltaTime, velocity); @@ -1040,6 +1055,48 @@ namespace Barotrauma } } break; + case AIBehaviorAfterAttack.IdleUntilCanAttack: + if (AttackingLimb.attack.SecondaryCoolDown <= 0) + { + // No (valid) secondary cooldown defined. + UpdateIdle(deltaTime, followLastTarget: false); + return; + } + else + { + if (AttackingLimb.attack.SecondaryCoolDownTimer <= 0) + { + // Don't allow attacking when the attack target has just changed. + if (_previousAiTarget != null && SelectedAiTarget != _previousAiTarget) + { + UpdateIdle(deltaTime, followLastTarget: false); + return; + } + else + { + // If the secondary cooldown is defined and expired, check if we can switch the attack + var newLimb = GetAttackLimb(attackWorldPos, AttackingLimb); + if (newLimb != null) + { + // Attack with the new limb + AttackingLimb = newLimb; + } + else + { + // No new limb was found. + UpdateIdle(deltaTime, followLastTarget: false); + return; + } + } + } + else + { + // Cooldown not yet expired + UpdateIdle(deltaTime, followLastTarget: false); + return; + } + } + break; case AIBehaviorAfterAttack.FollowThrough: UpdateFallBack(attackWorldPos, deltaTime, followThrough: true); return; @@ -1145,7 +1202,56 @@ namespace Barotrauma // Check that we can reach the target distance = toTarget.Length(); canAttack = distance < AttackingLimb.attack.Range; - if (!canAttack && !IsCoolDownRunning) + if (canAttack) + { + if (AttackingLimb.attack.Ranged) + { + // Check that is facing the target + float offset = AttackingLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2; + Vector2 forward = VectorExtensions.Forward(AttackingLimb.body.TransformedRotation - offset * Character.AnimController.Dir); + float angle = VectorExtensions.Angle(forward, toTarget); + canAttack = angle < MathHelper.ToRadians(AttackingLimb.attack.RequiredAngle); + if (canAttack && AttackingLimb.attack.AvoidFriendlyFire) + { + float minDistance = MathUtils.Pow(ConvertUnits.ToDisplayUnits(Character.AnimController.Collider.GetMaxExtent() * 3), 2); + bool IsFarEnough(Character other) => Vector2.DistanceSquared(Character.WorldPosition, other.WorldPosition) > minDistance; + if (SwarmBehavior != null) + { + canAttack = SwarmBehavior.Members.All(c => c == Character || IsFarEnough(c)); + } + else + { + canAttack = Character.CharacterList.All(c => c == Character || !IsFriendly(Character, c) || IsFarEnough(c)); + } + if (canAttack) + { + canAttack = !IsBlocked(attackSimPos) && !IsBlocked(AttackingLimb.SimPosition + forward * ConvertUnits.ToSimUnits(AttackingLimb.attack.Range)); + + bool IsBlocked(Vector2 targetPosition) + { + foreach (var body in Submarine.PickBodies(AttackingLimb.SimPosition, targetPosition, myBodies, Physics.CollisionCharacter)) + { + Character hitTarget = null; + if (body.UserData is Character c) + { + hitTarget = c; + } + else if (body.UserData is Limb limb) + { + hitTarget = limb.character; + } + if (hitTarget != null && !hitTarget.IsDead && IsFriendly(Character, hitTarget)) + { + return true; + } + } + return false; + } + } + } + } + } + else if (!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. @@ -1160,29 +1266,11 @@ namespace Barotrauma } } } - Limb steeringLimb = canAttack ? AttackingLimb : null; + Limb steeringLimb = canAttack && !AttackingLimb.attack.Ranged ? AttackingLimb : null; if (steeringLimb == null) { - // If the attacking limb is a hand or claw, for example, using it as the steering limb can end in the result where the character circles around the target. For example the Hammerhead steering with the claws when it should use the torso. - // If we always use the main limb, this causes the character to seek the target with it's torso/head, when it should not. For example Mudraptor steering with it's belly, when it should use it's head. - // So let's use the one that's closer to the attacking limb. - var torso = Character.AnimController.GetLimb(LimbType.Torso); - var head = Character.AnimController.GetLimb(LimbType.Head); - if (AttackingLimb == null) - { - steeringLimb = head ?? torso; - } - else - { - if (head != null && torso != null) - { - steeringLimb = Vector2.DistanceSquared(AttackingLimb.SimPosition, head.SimPosition) < Vector2.DistanceSquared(AttackingLimb.SimPosition, torso.SimPosition) ? head : torso; - } - else - { - steeringLimb = head ?? torso; - } - } + // If the attacking limb is a hand or claw, for example, using it as the steering limb can end in the result where the character circles around the target. + steeringLimb = Character.AnimController.GetLimb(LimbType.Head) ?? Character.AnimController.GetLimb(LimbType.Torso); } if (steeringLimb == null) @@ -1190,7 +1278,7 @@ namespace Barotrauma State = AIState.Idle; return; } - + if (AttackingLimb != null && AttackingLimb.attack.Retreat) { UpdateFallBack(attackWorldPos, deltaTime, false); @@ -1250,6 +1338,25 @@ namespace Barotrauma } else { + if (selectedTargetingParams.SweepDistance > 0) + { + 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; + } + } SteeringManager.SteeringSeek(steerPos, 10); SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15); } @@ -1268,7 +1375,7 @@ namespace Barotrauma { IsSteeringThroughGap = true; wallTarget = null; - LatchOntoAI?.DeattachFromBody(cooldown: 2); + LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 2); Character.AnimController.ReleaseStuckLimbs(); Hull targetHull = section.gap?.FlowTargetHull; float maxDistance = Math.Min(wall.Rect.Width, wall.Rect.Height); @@ -1308,6 +1415,7 @@ namespace Barotrauma { if (limb == ignoredLimb) { continue; } if (limb.IsSevered || limb.IsStuck) { continue; } + if (limb.Disabled) { continue; } var attack = limb.attack; if (attack == null) { continue; } if (attack.CoolDownTimer > 0) { continue; } @@ -1318,6 +1426,17 @@ namespace Barotrauma if (attack.Conditionals.Any(c => !c.Matches(se))) { continue; } } if (attack.Conditionals.Any(c => c.TargetSelf && !c.Matches(Character))) { continue; } + if (attack.Ranged) + { + // Check that is approximately facing the target + Vector2 attackLimbPos = Character.AnimController.SimplePhysicsEnabled ? Character.WorldPosition : limb.WorldPosition; + Vector2 toTarget = attackWorldPos - attackLimbPos; + float offset = limb.Params.GetSpriteOrientation() - MathHelper.PiOver2; + Vector2 forward = VectorExtensions.Forward(limb.body.TransformedRotation - offset * Character.AnimController.Dir); + float angle = VectorExtensions.Angle(forward, toTarget); + if (angle > MathHelper.ToRadians(attack.RequiredAngle)) { continue; } + } + if (AIParams.RandomAttack) { attackLimbs.Add(limb); @@ -1407,12 +1526,21 @@ namespace Barotrauma 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.Submarine.PhysicsBody.FarseerBody, wall.Submarine, ConvertUnits.ToSimUnits(sectionPos), attachTargetNormal); + 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 (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); + } } } } @@ -1450,13 +1578,13 @@ namespace Barotrauma bool wasLatched = IsLatchedOnSub; Character.AnimController.ReleaseStuckLimbs(); - LatchOntoAI?.DeattachFromBody(cooldown: 1); + LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 1); if (attacker == null || attacker.AiTarget == null || attacker.Removed || attacker.IsDead) { return; } bool isFriendly = IsFriendly(Character, attacker); if (wasLatched) { State = AIState.Escape; - avoidTimer = AIParams.AvoidTime * Rand.Range(0.75f, 1.25f); + avoidTimer = AIParams.AvoidTime * 0.5f * Rand.Range(0.75f, 1.25f); if (!isFriendly) { SelectTarget(attacker.AiTarget); @@ -2029,6 +2157,8 @@ 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.State == AIState.Observe || targetParams.State == AIState.Eat) { if (targetCharacter != null && targetCharacter.Submarine != Character.Submarine) @@ -2037,10 +2167,7 @@ namespace Barotrauma continue; } } - if (aiTarget.Entity is Item targetItem && targetParams.IgnoreContained && targetItem.ParentInventory != null) - { - continue; - } + if (aiTarget.Entity is Item targetItem && targetParams.IgnoreContained && targetItem.ParentInventory != null) { continue; } valueModifier *= targetParams.Priority; if (valueModifier == 0.0f) { continue; } @@ -2180,7 +2307,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++) @@ -2194,7 +2321,7 @@ namespace Barotrauma if (releaseTarget) { wallTarget = null; - LatchOntoAI.DeattachFromBody(cooldown: 1); + LatchOntoAI.DeattachFromBody(reset: true, cooldown: 1); } } else @@ -2408,7 +2535,7 @@ namespace Barotrauma protected override void OnStateChanged(AIState from, AIState to) { - LatchOntoAI?.DeattachFromBody(); + LatchOntoAI?.DeattachFromBody(reset: true); Character.AnimController.ReleaseStuckLimbs(); escapeTarget = null; AttackingLimb = null; @@ -2449,14 +2576,15 @@ namespace Barotrauma foreach (var limb in Character.AnimController.Limbs) { if (limb.IsSevered) { continue; } + if (limb.Disabled) { continue; } if (limb.attack == null) { continue; } if (!canAttackWalls) { - canAttackWalls = limb.attack.IsValidTarget(AttackTarget.Structure) && limb.attack.StructureDamage > 0; + canAttackWalls = limb.attack.IsValidTarget(AttackTarget.Structure) && (limb.attack.StructureDamage > 0 || limb.attack.Ranged); } if (!canAttackDoors) { - canAttackDoors = limb.attack.IsValidTarget(AttackTarget.Structure) && limb.attack.ItemDamage > 0; + canAttackDoors = limb.attack.IsValidTarget(AttackTarget.Structure) && (limb.attack.ItemDamage > 0 || limb.attack.Ranged); } if (!canAttackCharacters) { @@ -2503,7 +2631,7 @@ namespace Barotrauma } } - private bool CanPassThroughHole(Structure wall, int sectionIndex) + public bool CanPassThroughHole(Structure wall, int sectionIndex) { if (!wall.SectionBodyDisabled(sectionIndex)) return false; int holeCount = 1; @@ -2546,6 +2674,7 @@ namespace Barotrauma foreach (Limb limb in targetLimbs) { if (limb.IsSevered) { continue; } + if (limb.Hidden) { continue; } float dist = Vector2.DistanceSquared(limb.WorldPosition, attackLimb.WorldPosition) / Math.Max(limb.AttackPriority, 0.1f); if (dist < closestDist) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 541f78e66..236e2ad6f 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; @@ -37,12 +38,51 @@ namespace Barotrauma private float respondToAttackTimer; private const float RespondToAttackInterval = 1.0f; + private bool wasConscious; + + private bool freezeAI; + + private readonly float maxSteeringBuffer = 5000; + private readonly float minSteeringBuffer = 500; + private readonly float steeringBufferIncreaseSpeed = 100; + private float steeringBuffer; + + private readonly float obstacleRaycastInterval = 1; + private float obstacleRaycastTimer; /// /// List of previous attacks done to this character /// private readonly Dictionary previousAttackResults = new Dictionary(); + private readonly SteeringManager outsideSteering, insideSteering; + + 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 float CurrentHullSafety { get; private set; } = 100; + + private readonly Dictionary damageDoneByAttacker = new Dictionary(); + private readonly HashSet attackers = new HashSet(); + + private readonly Dictionary knownHulls = new Dictionary(); private class HullSafety { public float safety; @@ -72,35 +112,6 @@ namespace Barotrauma } } - private readonly Dictionary knownHulls = new Dictionary(); - - private SteeringManager outsideSteering, insideSteering; - - 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 float CurrentHullSafety { get; private set; } = 100; - - private readonly Dictionary damageDoneByAttacker = new Dictionary(); - private readonly HashSet attackers = new HashSet(); - public HumanAIController(Character c) : base(c) { if (!c.IsHuman) @@ -117,8 +128,6 @@ namespace Barotrauma partial void InitProjSpecific(); - private bool freezeAI; - public override void Update(float deltaTime) { if (DisableCrewAI || Character.Removed) { return; } @@ -146,6 +155,8 @@ namespace Barotrauma } if (isIncapacitated) { return; } + wasConscious = true; + respondToAttackTimer -= deltaTime; if (respondToAttackTimer <= 0.0f) { @@ -176,15 +187,38 @@ namespace Barotrauma IgnoredItems.Clear(); } - // Use the pathfinding also outside of the sub, but not farther than the extents of the sub + 500 units. - if (Character.Submarine != null || 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 + 500, 2)) + 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; + + if (Character.Submarine == null && hasValidPath) + { + obstacleRaycastTimer -= deltaTime; + if (obstacleRaycastTimer <= 0) + { + 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()) + { + 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 || hasValidPath && IsCloseEnoughToTargetSub(maxSteeringBuffer) || IsCloseEnoughToTargetSub(steeringBuffer)) { if (steeringManager != insideSteering) { insideSteering.Reset(); } steeringManager = insideSteering; + steeringBuffer += steeringBufferIncreaseSpeed * deltaTime; } else { @@ -193,7 +227,9 @@ namespace Barotrauma outsideSteering.Reset(); } steeringManager = outsideSteering; + steeringBuffer = minSteeringBuffer; } + steeringBuffer = Math.Clamp(steeringBuffer, minSteeringBuffer, maxSteeringBuffer); AnimController.Crouching = shouldCrouch; CheckCrouching(deltaTime); @@ -369,7 +405,8 @@ namespace Barotrauma if (!NeedsDivingGear(Character.CurrentHull, out bool needsSuit) || !needsSuit || oxygenLow) { bool shouldKeepTheGearOn = Character.AnimController.HeadInWater - || Character.Submarine.TeamID != Character.TeamID && Character.Submarine.TeamID != Character.TeamType.FriendlyNPC + || Character.Submarine == 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); @@ -435,6 +472,7 @@ namespace Barotrauma if (oxygenLow || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority) { divingSuit.Drop(Character); + HandleRelocation(divingSuit); } else if (findItemState == FindItemState.None || findItemState == FindItemState.DivingSuit) { @@ -461,6 +499,7 @@ namespace Barotrauma else { divingSuit.Drop(Character); + HandleRelocation(divingSuit); } } } @@ -478,6 +517,7 @@ namespace Barotrauma if (ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority) { mask.Drop(Character); + HandleRelocation(mask); } else if (findItemState == FindItemState.None || findItemState == FindItemState.DivingMask) { @@ -501,6 +541,7 @@ namespace Barotrauma else { mask.Drop(Character); + HandleRelocation(mask); } } } @@ -548,6 +589,7 @@ namespace Barotrauma else { item.Drop(Character); + HandleRelocation(item); } } } @@ -556,6 +598,62 @@ namespace Barotrauma } } + private readonly HashSet itemsToRelocate = new HashSet(); + + private void HandleRelocation(Item item) + { + if (item.Submarine?.TeamID == Character.TeamType.FriendlyNPC) + { + if (itemsToRelocate.Contains(item)) { return; } + itemsToRelocate.Add(item); + if (item.Submarine.ConnectedDockingPorts.TryGetValue(Submarine.MainSub, out DockingPort myPort)) + { + myPort.OnUnDocked += Relocate; + } + var campaign = GameMain.GameSession.Campaign; + if (campaign != null) + { + // In the campaign mode, undocking happens after leaving the outpost, so we can't use that. + campaign.BeforeLevelLoading += Relocate; + } + } + + void Relocate() + { + if (item == null || item.Removed) { return; } + if (!itemsToRelocate.Contains(item)) { return; } + var mainSub = Submarine.MainSub; + if (item.ParentInventory != null) + { + if (item.ParentInventory.Owner is Character c) + { + if (c.TeamID == Character.TeamType.Team1 || c.TeamID == Character.TeamType.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; + } + } + else if (item.ParentInventory.Owner.Submarine == mainSub) + { + // Placed inside an inventory that's already in the main sub. + return; + } + } + // Laying on ground inside the main sub. + if (item.Submarine == mainSub) + { + return; + } + WayPoint wp = WayPoint.GetRandom(SpawnType.Cargo, null, mainSub); + if (wp != null) + { + item.Submarine = mainSub; + item.SetTransform(wp.SimPosition, 0.0f); + } + itemsToRelocate.Remove(item); + } + } + public void ReequipUnequipped() { foreach (var item in unequippedItems) @@ -585,6 +683,7 @@ namespace Barotrauma suitableContainer = null; if (character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: ignoredItems, customPriorityFunction: i => { + if (i.IsThisOrAnyContainerIgnoredByAI()) { return 0; } var container = i.GetComponent(); if (container == null) { return 0; } if (container.Inventory.IsFull()) { return 0; } @@ -746,6 +845,13 @@ namespace Barotrauma 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) { @@ -1003,7 +1109,7 @@ namespace Barotrauma { var objective = new AIObjectiveCombat(Character, attacker, mode, objectiveManager) { - HoldPosition = Character.Info?.Job?.Prefab.Identifier == "watchman", + HoldPosition = Character.Info?.Job?.Prefab.Identifier == "watchman" || Character.CurrentHull == null && ObjectiveManager.IsCurrentOrder(), abortCondition = abortCondition, allowHoldFire = allowHoldFire, }; @@ -1168,7 +1274,7 @@ namespace Barotrauma needsSuit = true; return true; } - if (hull.WaterPercentage > 60 || hull.OxygenPercentage < CharacterHealth.LowOxygenThreshold) + if (hull.WaterPercentage > 60 || hull.Oxygen < CharacterHealth.LowOxygenThreshold) { return true; } @@ -1223,6 +1329,14 @@ namespace Barotrauma } //if (!otherCharacter.IsFacing(thief.WorldPosition)) { continue; } if (!otherCharacter.CanSeeCharacter(thief)) { continue; } + // Don't react if the player is taking an extinguisher and there's any fires on the sub, or diving gear when the sub is flooding + // -> allow them to use the emergency items + if (character.Submarine != null) + { + var connectedHulls = character.Submarine.GetHulls(alsoFromConnectedSubs: true); + if (item.HasTag("fireextinguisher") && connectedHulls.Any(h => h.FireSources.Any())) { continue; } + if (item.HasTag("diving") && connectedHulls.Any(h => h.ConnectedGaps.Any(g => AIObjectiveFixLeaks.IsValidTarget(g, thief)))) { continue; } + } if (!someoneSpoke && !character.IsIncapacitated && character.Stun <= 0.0f) { if (!item.StolenDuringRound && GameMain.GameSession?.Campaign?.Map?.CurrentLocation != null) @@ -1236,8 +1350,6 @@ namespace Barotrauma otherCharacter.Speak(TextManager.Get("dialogstealwarning"), null, Rand.Range(0.5f, 1.0f), "thief", 10.0f); someoneSpoke = true; } - // Don't react if the player is taking an extinguisher and there's any fires on the sub -> allow them to use the emergency items - if (item.HasTag("fireextinguisher") && character.Submarine.GetHulls(alsoFromConnectedSubs: true).Any(h => h.FireSources.Any())) { continue; } // React if we are security if (!TriggerSecurity(otherHumanAI)) { @@ -1425,7 +1537,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) { @@ -1464,7 +1576,17 @@ namespace Barotrauma // The hull safety decreases 90% per enemy up to 100% (TODO: test smaller percentages) enemyFactor = MathHelper.Lerp(1, 0, MathHelper.Clamp(enemyCount * 0.9f, 0, 1)); } - float safety = oxygenFactor * waterFactor * fireFactor * enemyFactor; + float dangerousItemsFactor = 1f; + 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); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs index 32c922a20..12b112cd7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs @@ -117,7 +117,7 @@ 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() { @@ -294,7 +294,13 @@ 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.NonInteractable) + { + 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) @@ -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)) { @@ -437,7 +441,7 @@ namespace Barotrauma if (door.IsOpen) { return true; } if (door.Item.NonInteractable) { return false; } if (CanBreakDoors) { return true; } - if (door.IsStuck) { return false; } + if (door.IsStuck || door.IsJammed) { return false; } if (!canOpenDoors || character.LockHands) { return false; } if (door.HasIntegratedButtons) { @@ -719,7 +723,10 @@ namespace Barotrauma if (wander) { SteeringWander(); - SteeringAvoid(deltaTime, lookAheadDistance: ConvertUnits.ToSimUnits(wallAvoidDistance), 5); + if (inWater) + { + SteeringAvoid(deltaTime, lookAheadDistance: ConvertUnits.ToSimUnits(wallAvoidDistance), 5); + } } if (!inWater) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs index 27f2a51a0..9ffdcf361 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs @@ -15,15 +15,17 @@ namespace Barotrauma private float raycastTimer; - private Body attachTargetBody; + private Structure targetWall; + private Body targetBody; private Vector2 attachSurfaceNormal; - private Submarine attachTargetSubmarine; + private Submarine targetSubmarine; + private readonly Character character; public bool AttachToSub { get; private set; } public bool AttachToWalls { get; private set; } - private float minDeattachSpeed = 3.0f, maxDeattachSpeed = 10.0f; - private float damageOnDetach = 0.0f, detachStun = 0.0f; + private readonly float minDeattachSpeed, maxDeattachSpeed; + private readonly float damageOnDetach, detachStun; private float deattachTimer; private Vector2 wallAttachPos; @@ -35,13 +37,8 @@ namespace Barotrauma private float attachLimbRotation; private float jointDir; - - private List attachJoints = new List(); - public List AttachJoints - { - get { return attachJoints; } - } + public List AttachJoints { get; } = new List(); public Vector2? WallAttachPos { @@ -49,19 +46,16 @@ namespace Barotrauma private set; } - public bool IsAttached - { - get { return attachJoints.Count > 0; } - } + public bool IsAttached => AttachJoints.Count > 0; - public bool IsAttachedToSub => IsAttached && (attachTargetBody?.UserData is Submarine || attachTargetBody?.UserData is Entity entity && entity.Submarine != null); + public bool IsAttachedToSub => IsAttached && targetSubmarine != null; public LatchOntoAI(XElement element, EnemyAIController enemyAI) { AttachToWalls = element.GetAttributeBool("attachtowalls", false); AttachToSub = element.GetAttributeBool("attachtosub", false); - minDeattachSpeed = element.GetAttributeFloat("mindeattachspeed", 3.0f); - maxDeattachSpeed = Math.Max(minDeattachSpeed, element.GetAttributeFloat("maxdeattachspeed", 10.0f)); + minDeattachSpeed = element.GetAttributeFloat("mindeattachspeed", 5.0f); + maxDeattachSpeed = Math.Max(minDeattachSpeed, element.GetAttributeFloat("maxdeattachspeed", 8.0f)); damageOnDetach = element.GetAttributeFloat("damageondetach", 0.0f); detachStun = element.GetAttributeFloat("detachstun", 0.0f); localAttachPos = ConvertUnits.ToSimUnits(element.GetAttributeVector2("localattachpos", Vector2.Zero)); @@ -81,45 +75,47 @@ namespace Barotrauma attachLimb = enemyAI.Character.AnimController.MainLimb; } + character = enemyAI.Character; enemyAI.Character.OnDeath += OnCharacterDeath; } - public void SetAttachTarget(Body attachTarget, Submarine attachTargetSub, Vector2 attachPos, Vector2 attachSurfaceNormal) + public void SetAttachTarget(Structure wall, Vector2 attachPos, Vector2 attachSurfaceNormal) { - attachTargetBody = attachTarget; - attachTargetSubmarine = attachTargetSub; + if (wall == null) { return; } + var sub = wall.Submarine; + if (sub == null) { return; } + targetWall = wall; + 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(); - WallAttachPos = null; + DeattachFromBody(reset: true); return; } - if (attachJoints.Count > 0) + if (AttachJoints.Count > 0) { if (Math.Sign(attachLimb.Dir) != Math.Sign(jointDir)) { - attachJoints[0].LocalAnchorA = - new Vector2(-attachJoints[0].LocalAnchorA.X, attachJoints[0].LocalAnchorA.Y); - attachJoints[0].ReferenceAngle = -attachJoints[0].ReferenceAngle; + AttachJoints[0].LocalAnchorA = + new Vector2(-AttachJoints[0].LocalAnchorA.X, AttachJoints[0].LocalAnchorA.Y); + AttachJoints[0].ReferenceAngle = -AttachJoints[0].ReferenceAngle; jointDir = attachLimb.Dir; } - for (int i = 0; i < attachJoints.Count; i++) + for (int i = 0; i < AttachJoints.Count; i++) { //something went wrong, limb body is very far from the joint anchor -> deattach - if (Vector2.DistanceSquared(attachJoints[i].WorldAnchorB, attachJoints[i].BodyA.Position) > 10.0f * 10.0f) + if (Vector2.DistanceSquared(AttachJoints[i].WorldAnchorB, AttachJoints[i].BodyA.Position) > 10.0f * 10.0f) { #if DEBUG DebugConsole.ThrowError("Limb body of the character \"" + character.Name + "\" is very far from the attach joint anchor -> deattach"); #endif - DeattachFromBody(); + DeattachFromBody(reset: true); return; } } @@ -135,9 +131,9 @@ namespace Barotrauma } Vector2 transformedAttachPos = wallAttachPos; - if (character.Submarine == null && attachTargetSubmarine != null) + if (character.Submarine == null && targetSubmarine != null) { - transformedAttachPos += ConvertUnits.ToSimUnits(attachTargetSubmarine.Position); + transformedAttachPos += ConvertUnits.ToSimUnits(targetSubmarine.Position); } if (transformedAttachPos != Vector2.Zero) { @@ -167,12 +163,12 @@ namespace Barotrauma { if (MathUtils.GetLineIntersection(edge.Point1, edge.Point2, character.WorldPosition, cell.Center, out Vector2 intersection)) { - attachSurfaceNormal = edge.GetNormal(cell); - attachTargetBody = 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; } @@ -190,9 +186,9 @@ namespace Barotrauma wallAttachPos = Vector2.Zero; } - if (wallAttachPos == Vector2.Zero) + if (wallAttachPos == Vector2.Zero || targetBody == null) { - DeattachFromBody(); + DeattachFromBody(reset: false); } else { @@ -201,13 +197,13 @@ namespace Barotrauma if (squaredDistance < targetDistance * targetDistance) { //close enough to a wall -> attach - AttachToBody(character.AnimController.Collider, attachLimb, attachTargetBody, wallAttachPos); + AttachToBody(wallAttachPos); enemyAI.SteeringManager.Reset(); } else { //move closer to the wall - DeattachFromBody(); + DeattachFromBody(reset: false); enemyAI.SteeringManager.SteeringAvoid(deltaTime, 1.0f, 0.1f); enemyAI.SteeringManager.SteeringSeek(wallAttachPos); } @@ -217,57 +213,76 @@ namespace Barotrauma case AIState.Aggressive: if (enemyAI.AttackingLimb != null) { - if (AttachToSub && !enemyAI.IsSteeringThroughGap && wallAttachPos != Vector2.Zero && attachTargetBody != null) + if (AttachToSub && !enemyAI.IsSteeringThroughGap && wallAttachPos != Vector2.Zero && targetBody != null) { // is not attached or is attached to something else - if (!IsAttached || IsAttached && attachJoints[0].BodyB != attachTargetBody) + if (!IsAttached || IsAttached && AttachJoints[0].BodyB != targetBody) { if (Vector2.DistanceSquared(ConvertUnits.ToDisplayUnits(transformedAttachPos), enemyAI.AttackingLimb.WorldPosition) < enemyAI.AttackingLimb.attack.DamageRange * enemyAI.AttackingLimb.attack.DamageRange) { - AttachToBody(character.AnimController.Collider, attachLimb, attachTargetBody, transformedAttachPos); + AttachToBody(transformedAttachPos); } } } } break; default: - WallAttachPos = null; - DeattachFromBody(); + DeattachFromBody(reset: true); break; } - if (IsAttached && attachTargetBody != null && deattachTimer < 0.0f) + if (IsAttached && targetBody != null && targetWall != null && targetSubmarine != null && deattachTimer <= 0.0f) { - Entity entity = attachTargetBody.UserData as Entity; - Submarine attachedSub = entity is Submarine sub ? sub : entity?.Submarine; - if (attachedSub != null) + bool deattach = false; + // Deattach if the wall is broken enough where we are attached to + int targetSection = targetWall.FindSectionIndex(attachLimb.WorldPosition, world: true, clamp: true); + if (enemyAI.CanPassThroughHole(targetWall, targetSection)) { - float velocity = attachedSub.Velocity == Vector2.Zero ? 0.0f : attachedSub.Velocity.Length(); - float velocityFactor = (maxDeattachSpeed - minDeattachSpeed <= 0.0f) ? - Math.Sign(Math.Abs(velocity) - minDeattachSpeed) : - (Math.Abs(velocity) - minDeattachSpeed) / (maxDeattachSpeed - minDeattachSpeed); - - if (Rand.Range(0.0f, 1.0f) < velocityFactor) + deattach = true; + attachCooldown = 2; + } + if (!deattach) + { + // Deattach if the velocity is high + float velocity = targetSubmarine.Velocity == Vector2.Zero ? 0.0f : targetSubmarine.Velocity.Length(); + deattach = velocity > maxDeattachSpeed; + if (!deattach) { - DeattachFromBody(); - character.AddDamage(character.WorldPosition, new List() { AfflictionPrefab.InternalDamage.Instantiate(damageOnDetach) }, detachStun, true); - attachCooldown = 5.0f; + if (velocity > minDeattachSpeed) + { + float velocityFactor = (maxDeattachSpeed - minDeattachSpeed <= 0.0f) ? + Math.Sign(Math.Abs(velocity) - minDeattachSpeed) : + (Math.Abs(velocity) - minDeattachSpeed) / (maxDeattachSpeed - minDeattachSpeed); + + if (Rand.Range(0.0f, 1.0f) < velocityFactor) + { + deattach = true; + character.AddDamage(character.WorldPosition, new List() { AfflictionPrefab.InternalDamage.Instantiate(damageOnDetach) }, detachStun, true); + attachCooldown = detachStun * 2; + } + } } } - + if (deattach) + { + DeattachFromBody(reset: true); + } deattachTimer = 5.0f; } } - 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) + if (AttachJoints.Count > 0) { //already attached to the target body, no need to do anything - if (attachJoints[0].BodyB == targetBody) { return; } - DeattachFromBody(); + if (AttachJoints[0].BodyB == targetBody) { return; } + DeattachFromBody(reset: false); } jointDir = attachLimb.Dir; @@ -290,7 +305,7 @@ namespace Barotrauma CollideConnected = false, }; GameMain.World.Add(limbJoint); - attachJoints.Add(limbJoint); + AttachJoints.Add(limbJoint); // Limb scale is already taken into account when creating the collider. Vector2 colliderFront = collider.GetLocalFront(); @@ -309,25 +324,37 @@ namespace Barotrauma //Length = 0.1f }; GameMain.World.Add(colliderJoint); - attachJoints.Add(colliderJoint); + AttachJoints.Add(colliderJoint); } - public void DeattachFromBody(float cooldown = 0) + public void DeattachFromBody(bool reset, float cooldown = 0) { - foreach (Joint joint in attachJoints) + foreach (Joint joint in AttachJoints) { GameMain.World.Remove(joint); } - attachJoints.Clear(); + AttachJoints.Clear(); if (cooldown > 0) { attachCooldown = cooldown; } + if (reset) + { + Reset(); + } + } + + private void Reset() + { + targetWall = null; + targetSubmarine = null; + targetBody = null; + WallAttachPos = null; } private void OnCharacterDeath(Character character, CauseOfDeath causeOfDeath) { - DeattachFromBody(); + DeattachFromBody(reset: true); character.OnDeath -= OnCharacterDeath; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs index b63a77d34..9b3913f2a 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.CurrentOrder == this) + { + 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; @@ -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.CurrentOrder == 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 1f286492f..efea317ca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs @@ -20,6 +20,7 @@ namespace Barotrauma { if (battery == null) { return false; } var item = battery.Item; + if (item.IgnoreByAI) { return false; } if (item.NonInteractable) { return false; } if (item.Submarine == null) { return false; } if (item.CurrentHull == null) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs index c19f7afa5..6d02eb2ec 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs @@ -1,4 +1,5 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using System.Collections.Generic; using System.Linq; @@ -25,7 +26,7 @@ namespace Barotrauma { // 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; } @@ -55,15 +56,13 @@ namespace Barotrauma return true; } - public static bool IsValidTarget(Item item, Character 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 (character != null && !IsItemInsideValidSubmarine(item, character)) { return false; } - //var rootContainer = item.GetRootContainer(); - //// Only target items lying on the ground (= not inside a container) (do we need this check?) - //if (rootContainer != null) { return false; } var pickable = item.GetComponent(); if (pickable == null) { return false; } if (pickable is Holdable h && h.Attachable && h.Attached) { return false; } @@ -80,7 +79,39 @@ namespace Barotrauma return false; } } - return item.Prefab.PreferredContainers.Any(); + if (item.Prefab.PreferredContainers.None()) + { + 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) + { + foreach (var slotType in inv.SlotTypes) + { + if (allowedSlot.HasFlag(slotType)) + { + for (int i = 0; i < inv.Capacity; i++) + { + canEquip = true; + if (allowedSlot.HasFlag(inv.SlotTypes[i]) && inv.Items[i] != null) + { + canEquip = false; + break; + } + } + } + } + } + } + return canEquip; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index 7311434af..580f2e80d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -110,7 +110,7 @@ namespace Barotrauma private bool IsOffensiveOrArrest => initialMode == CombatMode.Offensive || initialMode == CombatMode.Arrest; private bool TargetEliminated => Enemy == null || Enemy.Removed || 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 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) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs index cde9a0e2e..99f27021e 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,7 +31,7 @@ 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; @@ -59,7 +60,7 @@ namespace Barotrauma protected override bool Check() { if (IsCompleted) { return true; } - if (container == null) + if (container == null || (container.Item != null && container.Item.IsThisOrAnyContainerIgnoredByAI())) { Abandon = true; return false; @@ -82,19 +83,24 @@ 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; protected override void Act(float deltaTime) { - if (container == null) + if (container == null || (container.Item != null && container.Item.IsThisOrAnyContainerIgnoredByAI())) { 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(container.Item, out _, checkLinked: false)) + if (!character.CanInteractWith(ItemToContain, checkLinked: false)) + { + Abandon = true; + return; + } + if (character.CanInteractWith(container.Item, checkLinked: false)) { if (RemoveEmpty) { @@ -108,29 +114,29 @@ namespace Barotrauma } } // Contain the item - if (itemToContain.ParentInventory == character.Inventory) + if (ItemToContain.ParentInventory == character.Inventory) { - if (!container.Inventory.CanBePut(itemToContain)) + if (!container.Inventory.CanBePut(ItemToContain)) { Abandon = true; } else { - character.Inventory.RemoveItem(itemToContain); - if (container.Inventory.TryPutItem(itemToContain, null)) + character.Inventory.RemoveItem(ItemToContain); + if (container.Inventory.TryPutItem(ItemToContain, null)) { IsCompleted = true; } else { - itemToContain.Drop(character); + ItemToContain.Drop(character); Abandon = true; } } } else { - if (container.Combine(itemToContain, character)) + if (container.Combine(ItemToContain, character)) { IsCompleted = true; } @@ -142,11 +148,11 @@ namespace Barotrauma } else { - // TODO: should we just use GetItem? TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(container.Item, character, objectiveManager, getDivingGearIfNeeded: AllowToFindDivingGear) { DialogueIdentifier = "dialogcannotreachtarget", - TargetName = container.Item.Name + TargetName = container.Item.Name, + abortCondition = () => !ItemToContain.IsOwnedBy(character) }, onAbandon: () => Abandon = true, onCompleted: () => RemoveSubObjective(ref goToObjective)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs index e095d18cf..0df695c01 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs @@ -78,12 +78,21 @@ namespace Barotrauma { TryAddSubObjective(ref getExtinguisherObjective, () => { - character.Speak(TextManager.Get("DialogFindExtinguisher"), null, 2.0f, "findextinguisher", 30.0f); - return new AIObjectiveGetItem(character, "fireextinguisher", objectiveManager, equip: true) + if (!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()) + { + getItemObjective.Abandoned += () => character.Speak(TextManager.Get("dialogcannotfindfireextinguisher"), null, 0.0f, "dialogcannotfindfireextinguisher", 10.0f); + }; + return getItemObjective; }); } else @@ -99,9 +108,12 @@ 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; + bool canSee = HumanAIController.VisibleHulls.Contains(fs.Hull) || character.CanSeeTarget(fs); + bool move = !inRange || !canSee; + if ((inRange && canSee) || useExtinquisherTimer > 0) { useExtinquisherTimer += deltaTime; if (useExtinquisherTimer > 2.0f) @@ -115,19 +127,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); @@ -136,15 +136,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 diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs index b877fd2ce..12518f633 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs @@ -38,6 +38,7 @@ namespace Barotrauma public static bool IsValidTarget(Hull hull, Character character) { if (hull == null) { return false; } + if (hull.IgnoreByAI) { return false; } if (hull.FireSources.None()) { return false; } if (hull.Submarine == null) { return false; } if (character.Submarine == null) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs index f33a2ed7f..fe13ffe97 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs @@ -46,6 +46,7 @@ namespace Barotrauma } return new AIObjectiveGetItem(character, gearTag, objectiveManager, equip: true) { + AllowStealing = true, AllowToFindDivingGear = false, AllowDangerousPressure = true }; @@ -85,8 +86,7 @@ namespace Barotrauma return new AIObjectiveContainItem(character, OXYGEN_SOURCE, targetItem.GetComponent(), objectiveManager, spawnItemIfNotFound: character.TeamID == Character.TeamType.FriendlyNPC) { AllowToFindDivingGear = false, - AllowDangerousPressure = true, - ConditionLevel = 0 + AllowDangerousPressure = true }; }, onAbandon: () => Abandon = true, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs index 1d98fcf52..00db83826 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs @@ -46,7 +46,7 @@ namespace Barotrauma } if (character.CurrentHull == null) { - Priority = objectiveManager.CurrentOrder is AIObjectiveGoTo && HumanAIController.HasDivingSuit(character) ? 0 : 100; + Priority = (objectiveManager.IsCurrentOrder() || objectiveManager.Objectives.Any(o => o is AIObjectiveCombat)) && HumanAIController.HasDivingSuit(character) ? 0 : 100; } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs index e95d3cabf..ef05c4219 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs @@ -65,7 +65,14 @@ namespace Barotrauma if (weldingTool == null) { TryAddSubObjective(ref getWeldingTool, () => new AIObjectiveGetItem(character, "weldingequipment", objectiveManager, equip: true, spawnItemIfNotFound: character.TeamID == Character.TeamType.FriendlyNPC), - onAbandon: () => Abandon = true, + onAbandon: () => + { + if (objectiveManager.IsCurrentOrder()) + { + character.Speak(TextManager.Get("dialogcannotfindweldingequipment"), null, 0.0f, "dialogcannotfindweldingequipment", 10.0f); + } + Abandon = true; + }, onCompleted: () => RemoveSubObjective(ref getWeldingTool)); return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs index 60f172712..cddc1dd30 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs @@ -71,6 +71,8 @@ namespace Barotrauma public static bool IsValidTarget(Gap gap, Character character) { 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 || 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 4eda798d4..ccb23c9e3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs @@ -18,13 +18,13 @@ namespace Barotrauma public float TargetCondition { get; set; } = 1; public bool AllowDangerousPressure { get; set; } - private string[] identifiersOrTags; + private readonly string[] identifiersOrTags; //if the item can't be found, spawn it in the character's inventory (used by outpost NPCs) private bool spawnItemIfNotFound = false; private Item targetItem; - private Item originalTarget; + private readonly Item originalTarget; private ISpatialEntity moveToTarget; private bool isDoneSeeking; public Item TargetItem => targetItem; @@ -32,13 +32,18 @@ namespace Barotrauma public string[] ignoredContainerIdentifiers; private AIObjectiveGoTo goToObjective; private float currItemPriority; - private bool checkInventory; + private readonly bool checkInventory; public static float DefaultReach = 100; public bool AllowToFindDivingGear { get; set; } = true; public bool MustBeSpecificItem { get; set; } + /// + /// Is the character allowed to take the item from somewhere else than their own sub (e.g. an outpost) + /// + public bool AllowStealing { get; set; } + public AIObjectiveGetItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, bool equip = true, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { @@ -246,9 +251,13 @@ namespace Barotrauma currSearchIndex++; var item = Item.ItemList[currSearchIndex]; Submarine itemSub = item.Submarine ?? item.ParentInventory?.Owner?.Submarine; - Submarine mySub = character.Submarine; if (itemSub == null) { continue; } + Submarine mySub = character.Submarine; if (mySub == null) { continue; } + if (!AllowStealing) + { + if (character.TeamID == Character.TeamType.FriendlyNPC != item.SpawnedInOutpost) { continue; } + } if (!CheckItem(item)) { continue; } if (ignoredContainerIdentifiers != null && item.Container != null) { @@ -339,6 +348,7 @@ namespace Barotrauma private bool CheckItem(Item item) { if (item.NonInteractable) { return false; } + if (item.IsThisOrAnyContainerIgnoredByAI()) { return false; } if (ignoredItems.Contains(item)) { return false; }; if (item.Condition < TargetCondition) { return false; } if (ItemFilter != null && !ItemFilter(item)) { return false; } @@ -362,14 +372,5 @@ namespace Barotrauma isDoneSeeking = false; currSearchIndex = 0; } - - protected override void OnAbandon() - { - base.OnAbandon(); - if (objectiveManager.CurrentOrder != null) - { - character.Speak(TextManager.Get("DialogCannotFindItem"), null, 0.0f, "cannotfinditem", 10.0f); - } - } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs index fc5852c73..1f3cac5fd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -30,6 +30,8 @@ namespace Barotrauma public bool followControlledCharacter; public bool mimic; + public float extraDistanceWhileSwimming; + public float extraDistanceOutsideSub; private float _closeEnough = 50; private readonly float minDistance = 50; /// @@ -37,7 +39,19 @@ namespace Barotrauma /// public float CloseEnough { - get { return _closeEnough; } + get + { + float dist = _closeEnough; + if (character.AnimController.InWater) + { + dist += extraDistanceWhileSwimming; + } + if (character.CurrentHull == null) + { + dist += extraDistanceOutsideSub; + } + return dist; + } set { _closeEnough = Math.Max(minDistance, value); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs index 87bca211c..1ced9115f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs @@ -21,14 +21,13 @@ namespace Barotrauma set { behavior = value; + if (behavior == BehaviorType.StayInHull && character.TeamID != Character.TeamType.FriendlyNPC) + { + DebugConsole.NewMessage($"AIObjectiveIdle.BehaviorType.StayInHull is implemented only for outpost NPCs. Using passive behavior for {character.Name} ({character.Info.Job.Prefab.Identifier})", color: Color.Red); + behavior = BehaviorType.Passive; + } switch (behavior) { - case BehaviorType.Active: - newTargetIntervalMin = 10; - newTargetIntervalMax = 20; - standStillMin = 2; - standStillMax = 10; - break; case BehaviorType.Passive: case BehaviorType.StayInHull: newTargetIntervalMin = 60; @@ -36,6 +35,18 @@ namespace Barotrauma standStillMin = 30; standStillMax = 60; break; + case BehaviorType.Active: + newTargetIntervalMin = 40; + newTargetIntervalMax = 60; + standStillMin = 20; + standStillMax = 40; + break; + case BehaviorType.Patrol: + newTargetIntervalMin = 15; + newTargetIntervalMax = 30; + standStillMin = 5; + standStillMax = 10; + break; } } } @@ -49,9 +60,10 @@ namespace Barotrauma public enum BehaviorType { - Active, + Patrol, Passive, - StayInHull + StayInHull, + Active } public Hull TargetHull { get; set; } private Hull currentTarget; @@ -307,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) { @@ -434,10 +446,11 @@ namespace Barotrauma targetHulls.Add(hull); float weight = hull.RectWidth; // Prefer rooms that are closer. Avoid rooms that are not in the same level. + // If the behavior is active, prefer rooms that are not close. float yDist = Math.Abs(character.WorldPosition.Y - hull.WorldPosition.Y); yDist = yDist > 100 ? yDist * 5 : 0; float dist = Math.Abs(character.WorldPosition.X - hull.WorldPosition.X) + yDist; - float distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 2500, dist)); + float distanceFactor = behavior == BehaviorType.Patrol ? MathHelper.Lerp(1, 0, MathUtils.InverseLerp(2500, 0, dist)) : MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 2500, dist)); float waterFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 100, hull.WaterPercentage * 2)); weight *= distanceFactor * waterFactor; hullWeights.Add(weight); @@ -474,7 +487,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/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs index b223a850d..56b5c4ac8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs @@ -245,7 +245,10 @@ namespace Barotrauma public void SortObjectives() { CurrentOrder?.GetPriority(); - Objectives.ForEach(o => o.GetPriority()); + for (int i = Objectives.Count - 1; i >= 0; i--) + { + Objectives[i].GetPriority(); + } if (Objectives.Any()) { Objectives.Sort((x, y) => y.Priority.CompareTo(x.Priority)); @@ -305,6 +308,8 @@ namespace Barotrauma newObjective = new AIObjectiveGoTo(orderGiver, character, this, repeat: true, priorityModifier: priorityModifier) { CloseEnough = Rand.Range(90, 100) + Rand.Range(50, 70) * Math.Min(HumanAIController.CountCrew(c => c.ObjectiveManager.CurrentOrder is AIObjectiveGoTo gotoOrder && gotoOrder.Target == orderGiver, onlyBots: true), 4), + extraDistanceOutsideSub = 100, + extraDistanceWhileSwimming = 100, AllowGoingOutside = true, IgnoreIfTargetDead = true, followControlledCharacter = orderGiver == character, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs index bc42743ab..0a9f3505d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs @@ -157,11 +157,20 @@ namespace Barotrauma Abandon = true; return; } - // Don't allow to operate an item that someone with a better skills already operates, unless this is an order - if (objectiveManager.CurrentOrder != this && HumanAIController.IsItemOperatedByAnother(target, out _)) + // If this is not an order... + if (objectiveManager.CurrentOrder != this) { - // Don't abandon - return; + // 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) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs index 332d34fda..14a30c4ad 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs @@ -27,6 +27,7 @@ namespace Barotrauma protected override bool Filter(Pump pump) { if (pump == null) { return false; } + if (pump.Item.IgnoreByAI) { return false; } if (pump.Item.NonInteractable) { return false; } if (pump.Item.HasTag("ballast")) { return false; } if (pump.Item.Submarine == null) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs index a1f950d59..5d0c597fd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs @@ -90,7 +90,12 @@ namespace Barotrauma { foreach (RelatedItem requiredItem in kvp.Value) { - subObjectives.Add(new AIObjectiveGetItem(character, requiredItem.Identifiers, objectiveManager, true)); + 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); + } + subObjectives.Add(getItemObjective); } } return; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs index 7755bf7fc..9f0588f44 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs @@ -148,6 +148,7 @@ namespace Barotrauma public static bool IsValidTarget(Item item, Character character) { if (item == null) { return false; } + if (item.IgnoreByAI) { return false; } if (item.NonInteractable) { return false; } if (item.IsFullCondition) { return false; } if (item.CurrentHull == null) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs index e1b8b47f4..923924892 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs @@ -94,7 +94,9 @@ namespace Barotrauma //if true, the order is issued to all available characters - public bool TargetAllCharacters; + public bool TargetAllCharacters { get; } + public bool IsReport => TargetAllCharacters && !MustSetTarget; + public readonly float FadeOutTime; @@ -132,11 +134,31 @@ namespace Barotrauma { get { - if (targetSpatialEntity == null) { targetSpatialEntity = TargetEntity ?? TargetPosition as ISpatialEntity; } + if (targetSpatialEntity == null) + { + if (TargetType == OrderTargetType.WallSection && WallSectionIndex.HasValue) + { + targetSpatialEntity = (TargetEntity as Structure)?.Sections[WallSectionIndex.Value]; + } + else + { + targetSpatialEntity = TargetEntity ?? TargetPosition as ISpatialEntity; + } + } return targetSpatialEntity; } } + public enum OrderTargetType + { + Entity, + Position, + WallSection + } + public OrderTargetType TargetType { get; } + public int? WallSectionIndex { get; } + public bool IsIgnoreOrder { get; } + public static void Init() { Prefabs = new Dictionary(); @@ -292,6 +314,7 @@ namespace Barotrauma IsPrefab = true; MustManuallyAssign = orderElement.GetAttributeBool("mustmanuallyassign", false); + IsIgnoreOrder = Identifier == "ignorethis" || Identifier == "unignorethis"; } /// @@ -299,7 +322,7 @@ namespace Barotrauma /// public Order(Order prefab, Entity targetEntity, ItemComponent targetItem, Character orderGiver = null, bool isAutonomous = false) { - Prefab = prefab; + Prefab = prefab.Prefab ?? prefab; Name = prefab.Name; Identifier = prefab.Identifier; @@ -317,6 +340,7 @@ namespace Barotrauma AppropriateSkill = prefab.AppropriateSkill; Category = prefab.Category; MustManuallyAssign = prefab.MustManuallyAssign; + IsIgnoreOrder = prefab.IsIgnoreOrder; OrderGiver = orderGiver; TargetEntity = targetEntity; @@ -337,12 +361,21 @@ namespace Barotrauma TargetItemComponent = targetItem; } + TargetType = OrderTargetType.Entity; + IsPrefab = false; } public Order(Order prefab, OrderTarget target, Character orderGiver = null) : this(prefab, targetEntity: null, targetItem: null, orderGiver) { TargetPosition = target; + TargetType = OrderTargetType.Position; + } + + public Order(Order prefab, Structure wall, int? sectionIndex, Character orderGiver = null) : this(prefab, targetEntity: wall, null, orderGiver: orderGiver) + { + WallSectionIndex = sectionIndex; + TargetType = OrderTargetType.WallSection; } public bool HasAppropriateJob(Character character) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs index 3bf84304a..92de10a84 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs @@ -7,41 +7,32 @@ namespace Barotrauma { class PathNode { - private readonly int wayPointID; - public int state; public PathNode Parent; - private Vector2 position; - public float F, G, H; - public List connections; + public readonly List connections = new List(); public List distances; public Vector2 TempPosition; public float TempDistance; - public WayPoint Waypoint { get; private set; } - - public Vector2 Position - { - get { return position; } - } + public readonly WayPoint Waypoint; + public readonly Vector2 Position; + public readonly int WayPointID; public override string ToString() { - return $"PathNode {wayPointID}"; + return $"PathNode {WayPointID}"; } public PathNode(WayPoint wayPoint) { - this.Waypoint = wayPoint; - this.position = wayPoint.SimPosition; - wayPointID = wayPoint.ID; - - connections = new List(); + Waypoint = wayPoint; + Position = wayPoint.SimPosition; + WayPointID = Waypoint.ID; } public static List GenerateNodes(List wayPoints) @@ -78,7 +69,7 @@ namespace Barotrauma node.distances = new List(); for (int i = 0; i < node.connections.Count; i++) { - node.distances.Add(Vector2.Distance(node.position, node.connections[i].position)); + node.distances.Add(Vector2.Distance(node.Position, node.connections[i].Position)); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs index 53ca4f47f..90c20a4f9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs @@ -404,13 +404,19 @@ namespace Barotrauma if (petBehavior != null) { petBehavior.Owner = owner; - var petBehaviorElement = subElement.Attribute("petbehavior"); + var petBehaviorElement = subElement.Element("petbehavior"); if (petBehaviorElement != null) { - petBehavior.Hunger = petBehaviorElement.GetAttributeFloat(50.0f); - petBehavior.Happiness = petBehaviorElement.GetAttributeFloat(50.0f); + petBehavior.Hunger = petBehaviorElement.GetAttributeFloat("hunger", 50.0f); + petBehavior.Happiness = petBehaviorElement.GetAttributeFloat("happiness", 50.0f); } } + + var inventoryElement = subElement.Element("inventory"); + if (inventoryElement != null) + { + pet.SpawnInventoryItems(pet.Inventory, inventoryElement); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs index c583b6dcf..d934f5822 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs @@ -19,9 +19,9 @@ 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, isNetworkPlayer, ragdoll) + : base(speciesName, position, seed, characterInfo, id: Entity.NullEntityID, isRemotePlayer: isNetworkPlayer, ragdollParams: ragdoll) { InitProjSpecific(); } @@ -62,21 +62,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 > EnableSimplePhysicsDistSqr) { AnimController.SimplePhysicsEnabled = true; } - else if (characterDist < DisableSimplePhysicsDistSqr) + else if (characterDistSqr < DisableSimplePhysicsDistSqr) { AnimController.SimplePhysicsEnabled = false; } @@ -90,50 +81,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 6be176ce8..b08df6dab 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs @@ -407,8 +407,8 @@ namespace Barotrauma { if (CurrentSwimParams == null) { return; } movement = TargetMovement; - - if (movement.LengthSquared() > 0.00001f) + bool isMoving = movement.LengthSquared() > 0.00001f; + if (isMoving) { float t = 0.5f; if (CurrentSwimParams.RotateTowardsMovement && VectorExtensions.Angle(VectorExtensions.Forward(Collider.Rotation + MathHelper.PiOver2), movement) > MathHelper.PiOver2) @@ -425,7 +425,7 @@ namespace Barotrauma mainLimb.PullJointEnabled = true; //mainLimb.PullJointWorldAnchorB = Collider.SimPosition; - if (movement.LengthSquared() < 0.00001f) + if (!isMoving) { WalkPos = MathHelper.SmoothStep(WalkPos, MathHelper.PiOver2, deltaTime * 5); mainLimb.PullJointWorldAnchorB = Collider.SimPosition; @@ -625,7 +625,8 @@ namespace Barotrauma if (limb.IsSevered) { continue; } if (Math.Abs(limb.Params.ConstantTorque) > 0) { - limb.body.SmoothRotate(MainLimb.Rotation + MathHelper.ToRadians(limb.Params.ConstantAngle) * Dir, limb.Mass * limb.Params.ConstantTorque, wrapAngle: true); + float movementFactor = Math.Max(character.AnimController.Collider.LinearVelocity.Length() * 0.5f, 1); + limb.body.SmoothRotate(MainLimb.Rotation + MathHelper.ToRadians(limb.Params.ConstantAngle) * Dir, limb.Mass * limb.Params.ConstantTorque * movementFactor, wrapAngle: true); } if (limb.Params.BlinkFrequency > 0) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index c77df67a5..b42f8dfa1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -1974,9 +1974,6 @@ namespace Barotrauma public override void UpdateUseItem(bool allowMovement, Vector2 handWorldPos) { - var leftHand = GetLimb(LimbType.LeftHand); - var rightHand = GetLimb(LimbType.RightHand); - useItemTimer = 0.5f; Anim = Animation.UsingConstruction; @@ -1999,13 +1996,21 @@ namespace Barotrauma handSimPos -= character.Submarine.SimPosition; } - leftHand.Disabled = true; - leftHand.PullJointEnabled = true; - leftHand.PullJointWorldAnchorB = handSimPos; + var leftHand = GetLimb(LimbType.LeftHand); + if (leftHand != null) + { + leftHand.Disabled = true; + leftHand.PullJointEnabled = true; + leftHand.PullJointWorldAnchorB = handSimPos; + } - rightHand.Disabled = true; - rightHand.PullJointEnabled = true; - rightHand.PullJointWorldAnchorB = handSimPos; + var rightHand = GetLimb(LimbType.RightHand); + if (rightHand != null) + { + rightHand.Disabled = true; + rightHand.PullJointEnabled = true; + rightHand.PullJointWorldAnchorB = handSimPos; + } } public override void Flip() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index 50c0eb472..c3ea7e533 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -227,7 +227,7 @@ namespace Barotrauma } } - bool IsValid(Limb limb) => limb != null && !limb.IsSevered && !limb.ignoreCollisions; + bool IsValid(Limb limb) => limb != null && !limb.IsSevered && !limb.IgnoreCollisions && !limb.Hidden; return mainLimb; } } @@ -1060,7 +1060,7 @@ namespace Barotrauma foreach (Limb limb in Limbs) { - if (limb.ignoreCollisions || limb.IsSevered) { continue; } + if (limb.IgnoreCollisions || limb.IsSevered) { continue; } try { @@ -1562,7 +1562,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 +1575,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 +1586,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); + } } } @@ -1634,7 +1635,8 @@ namespace Barotrauma protected void CheckDistFromCollider() { - float allowedDist = Math.Max(Math.Max(Collider.radius, Collider.width), Collider.height) * 2.0f; + float allowedDist = Math.Max(Math.Max(Collider.radius, Collider.width), Collider.height) * 2.0f; + allowedDist = Math.Max(allowedDist, 1.0f); float resetDist = allowedDist * 5.0f; Vector2 diff = Collider.SimPosition - MainLimb.SimPosition; @@ -1643,7 +1645,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 369b0fec5..91af8d0a2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -35,7 +35,8 @@ namespace Barotrauma PursueIfCanAttack, Pursue, FollowThrough, - FollowThroughUntilCanAttack + FollowThroughUntilCanAttack, + IdleUntilCanAttack } struct AttackResult @@ -117,12 +118,27 @@ namespace Barotrauma [Serialize(0f, true, description: "A random factor applied to all cooldowns. Example: 0.1 -> adds a random value between -10% and 10% of the cooldown. Min 0 (default), Max 1 (could disable or double the cooldown in extreme cases)."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)] public float CoolDownRandomFactor { get; private set; } = 0; + [Serialize(false, true), Editable] + public bool FullSpeedAfterAttack { get; private set; } + [Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)] public float StructureDamage { get; set; } [Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] public float ItemDamage { get; set; } + [Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] + public float LevelWallDamage { get; set; } + + [Serialize(false, true)] + public bool Ranged { get; set; } + + [Serialize(false, true, description:"Only affects ranged attacks.")] + public bool AvoidFriendlyFire { get; set; } + + [Serialize(20f, true)] + public float RequiredAngle { get; set; } + /// /// Legacy support. Use Afflictions. /// @@ -247,6 +263,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; @@ -270,7 +291,7 @@ namespace Barotrauma Range = range; DamageRange = range; - StructureDamage = structureDamage; + StructureDamage = LevelWallDamage = structureDamage; ItemDamage = itemDamage; } @@ -286,6 +307,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()) @@ -379,7 +407,7 @@ namespace Barotrauma ReloadAfflictions(element); } - public AttackResult DoDamage(Character attacker, IDamageable target, Vector2 worldPosition, float deltaTime, bool playSound = true) + public AttackResult DoDamage(Character attacker, IDamageable target, Vector2 worldPosition, float deltaTime, bool playSound = true, PhysicsBody sourceBody = null) { Character targetCharacter = target as Character; if (OnlyHumans) @@ -403,6 +431,7 @@ namespace Barotrauma foreach (StatusEffect effect in statusEffects) { + effect.sourceBody = sourceBody; // TODO: do we want to apply the effect at the world position or the entity positions in each cases? -> go through also other cases where status effects are applied if (effect.HasTargetType(StatusEffect.TargetType.This)) { @@ -423,14 +452,18 @@ namespace Barotrauma effect.Apply(effectType, deltaTime, targetCharacter, targetCharacter.AnimController.Limbs.Cast().ToList()); } } - if (target is Entity entity) + if (target is Entity targetEntity) { if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) || effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { var targets = new List(); effect.GetNearbyTargets(worldPosition, targets); - effect.Apply(ActionType.OnActive, deltaTime, entity, targets); + effect.Apply(effectType, deltaTime, targetEntity, targets); + } + if (effect.HasTargetType(StatusEffect.TargetType.UseTarget)) + { + effect.Apply(effectType, deltaTime, targetEntity, attacker, worldPosition); } } } @@ -438,7 +471,7 @@ namespace Barotrauma return attackResult; } - public AttackResult DoDamageToLimb(Character attacker, Limb targetLimb, Vector2 worldPosition, float deltaTime, bool playSound = true) + public AttackResult DoDamageToLimb(Character attacker, Limb targetLimb, Vector2 worldPosition, float deltaTime, bool playSound = true, PhysicsBody sourceBody = null) { if (targetLimb == null) { @@ -462,6 +495,7 @@ namespace Barotrauma foreach (StatusEffect effect in statusEffects) { + effect.sourceBody = sourceBody; if (effect.HasTargetType(StatusEffect.TargetType.This)) { effect.Apply(effectType, deltaTime, attacker, attacker); @@ -478,6 +512,17 @@ namespace Barotrauma { effect.Apply(effectType, deltaTime, targetLimb.character, targetLimb.character.AnimController.Limbs.Cast().ToList()); } + if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) || + effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) + { + var targets = new List(); + effect.GetNearbyTargets(worldPosition, targets); + effect.Apply(effectType, deltaTime, targetLimb.character, targets); + } + if (effect.HasTargetType(StatusEffect.TargetType.UseTarget)) + { + effect.Apply(effectType, deltaTime, targetLimb.character, attacker, worldPosition); + } } return attackResult; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 9d123be44..017ff62d0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -85,6 +85,7 @@ namespace Barotrauma /// public bool IsRemotePlayer { get; set; } + public bool IsLocalPlayer => Controlled == this; public bool IsPlayer => Controlled == this || IsRemotePlayer; public bool IsBot => !IsPlayer && AIController is HumanAIController humanAI && humanAI.Enabled; @@ -314,6 +315,7 @@ namespace Barotrauma set { hideFaceTimer = MathHelper.Clamp(hideFaceTimer + (value ? 1.0f : -0.5f), 0.0f, 10.0f); + if (info != null && info.IsDisguisedAsAnother != HideFace) info.CheckDisguiseStatus(true); } } @@ -744,9 +746,9 @@ namespace Barotrauma /// Is the character controlled by a remote player. /// Is the character controlled by AI. /// Ragdoll configuration file. If null, will select the default. - public static Character Create(CharacterInfo characterInfo, Vector2 position, string seed, bool isRemotePlayer = false, bool hasAi = true, RagdollParams ragdoll = null) + public static Character Create(CharacterInfo characterInfo, Vector2 position, string seed, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, bool hasAi = true, RagdollParams ragdoll = null) { - return Create(characterInfo.SpeciesName, position, seed, characterInfo, isRemotePlayer, hasAi, true, ragdoll); + return Create(characterInfo.SpeciesName, position, seed, characterInfo, id, isRemotePlayer, hasAi, true, ragdoll); } /// @@ -756,11 +758,12 @@ namespace Barotrauma /// Position in display units. /// RNG seed to use if the character config has randomizable parameters. /// The name, gender, etc of the character. Only used for humans, and if the parameter is not given, a random CharacterInfo is generated. + /// ID to assign to the character. If set to 0, automatically find an available ID. /// Is the character controlled by a remote player. /// Is the character controlled by AI. /// Should clients receive a network event about the creation of this character? /// Ragdoll configuration file. If null, will select the default. - public static Character Create(string speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, bool isRemotePlayer = false, bool hasAi = true, bool createNetworkEvent = true, RagdollParams ragdoll = null) + public static Character Create(string speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, bool hasAi = true, bool createNetworkEvent = true, RagdollParams ragdoll = null) { if (speciesName.EndsWith(".xml", StringComparison.OrdinalIgnoreCase)) { @@ -790,7 +793,7 @@ namespace Barotrauma } else { - newCharacter = new Character(speciesName, position, seed, characterInfo, isRemotePlayer, ragdoll); + newCharacter = new Character(speciesName, position, seed, characterInfo, id: id, isRemotePlayer: isRemotePlayer, ragdollParams: ragdoll); } float healthRegen = newCharacter.Params.Health.ConstantHealthRegeneration; @@ -830,8 +833,8 @@ namespace Barotrauma return newCharacter; } - protected Character(string speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, bool isRemotePlayer = false, RagdollParams ragdollParams = null) - : base(null) + protected Character(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); @@ -1468,7 +1471,7 @@ namespace Barotrauma AnimController.ReleaseStuckLimbs(); if (AIController != null && AIController is EnemyAIController enemyAI) { - enemyAI.LatchOntoAI?.DeattachFromBody(); + enemyAI.LatchOntoAI?.DeattachFromBody(reset: true); } } #endif @@ -1524,6 +1527,7 @@ namespace Barotrauma var validLimbs = AnimController.Limbs.Where(l => { if (l.IsSevered || l.IsStuck) { return false; } + if (l.Disabled) { return false; } var attack = l.attack; if (attack == null) { return false; } if (attack.CoolDownTimer > 0) { return false; } @@ -1636,6 +1640,7 @@ namespace Barotrauma foreach (Limb limb in target.AnimController.Limbs) { if (limb.IsSevered || limb == target.AnimController.MainLimb) { continue; } + if (limb.Hidden) { continue; } Vector2 limbDir = limb.WorldPosition - WorldPosition; float leftDot = Vector2.Dot(limbDir, leftDir); if (leftDot > leftMostDot) @@ -1906,9 +1911,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) @@ -1998,7 +2003,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) { @@ -2019,8 +2024,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; @@ -2549,21 +2554,30 @@ namespace Barotrauma } OxygenAvailable += MathHelper.Clamp(hullAvailableOxygen - oxygenAvailable, -deltaTime * 50.0f, deltaTime * 50.0f); } - } - 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++) @@ -2582,7 +2596,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; @@ -3430,6 +3444,75 @@ namespace Barotrauma } } + public void SpawnInventoryItems(Inventory inventory, XElement itemData) + { + SpawnInventoryItemsRecursive(inventory, itemData); + } + + private void SpawnInventoryItemsRecursive(Inventory inventory, XElement element) + { + foreach (XElement itemElement in element.Elements()) + { + var newItem = Item.Load(itemElement, inventory.Owner.Submarine, createNetworkEvent: true, idRemap: IdRemap.DiscardId); + if (newItem == null) { continue; } + + if (!MathUtils.NearlyEqual(newItem.Condition, newItem.MaxCondition) && + GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) + { + GameMain.NetworkMember.CreateEntityEvent(newItem, new object[] { NetEntityEvent.Type.Status }); + } +#if SERVER + newItem.GetComponent()?.SyncHistory(); +#endif + int[] slotIndices = itemElement.GetAttributeIntArray("i", new int[] { 0 }); + if (!slotIndices.Any()) + { + DebugConsole.ThrowError("Invalid inventory data in character \"" + Name + "\" - no slot indices found"); + continue; + } + + //make sure there's no other item in the slot + //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) + { + 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); + } + } + + 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++) + { + if (slotIndices.Contains(i)) + { + inventory.Items[i] = newItem; + } + else if (inventory.Items[i] == newItem) + { + inventory.Items[i] = null; + } + } + + int itemContainerIndex = 0; + var itemContainers = newItem.GetComponents().ToList(); + foreach (XElement childInvElement in itemElement.Elements()) + { + if (itemContainerIndex >= itemContainers.Count) break; + if (!childInvElement.Name.ToString().Equals("inventory", StringComparison.OrdinalIgnoreCase)) { continue; } + SpawnInventoryItemsRecursive(itemContainers[itemContainerIndex].Inventory, childInvElement); + 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 71297fecd..aa4e3ce3a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -160,10 +160,12 @@ namespace Barotrauma { if (Character == null || !Character.HideFace) { + IsDisguised = IsDisguisedAsAnother = false; return Name; } else if ((GameMain.NetworkMember != null && !GameMain.NetworkMember.ServerSettings.AllowDisguises)) { + IsDisguised = IsDisguisedAsAnother = false; return Name; } @@ -263,6 +265,62 @@ namespace Barotrauma } } + public bool IsDisguised = false; + public bool IsDisguisedAsAnother = false; + + public void CheckDisguiseStatus(bool handleBuff, IdCard idCard = null) + { + if (Character == null) { return; } + + string currentlyDisplayedName = DisplayName; + + IsDisguised = currentlyDisplayedName == disguiseName; + IsDisguisedAsAnother = !IsDisguised && currentlyDisplayedName != Name; + + if (IsDisguisedAsAnother) + { + if (handleBuff) + { + Character.CharacterHealth.ApplyAffliction(Character.AnimController.GetLimb(LimbType.Head), AfflictionPrefab.List.FirstOrDefault(a => a.Identifier.Equals("disguised", StringComparison.OrdinalIgnoreCase)).Instantiate(100f)); + } + + if (idCard != null) + { +#if CLIENT + GetDisguisedSprites(idCard); +#endif + return; + } + + if (Character.Inventory != null) + { + int cardSlotIndex = Character.Inventory.FindLimbSlot(InvSlotType.Card); + if (cardSlotIndex >= 0) + { + idCard = Character.Inventory.Items[cardSlotIndex].GetComponent(); + + if (idCard != null) + { +#if CLIENT + GetDisguisedSprites(idCard); +#endif + return; + } + } + } + } + +#if CLIENT + disguisedJobIcon = null; + disguisedPortrait = null; +#endif + + if (handleBuff) + { + Character.CharacterHealth.ReduceAffliction(Character.AnimController.GetLimb(LimbType.Head), "disguised", 100f); + } + } + private List attachmentSprites; public List AttachmentSprites { @@ -582,7 +640,7 @@ namespace Barotrauma return id; } - public IEnumerable FilterByTypeAndHeadID(IEnumerable elements, WearableType targetType) + public IEnumerable FilterByTypeAndHeadID(IEnumerable elements, WearableType targetType, int headSpriteId) { if (elements == null) { return elements; } return elements.Where(e => @@ -590,16 +648,16 @@ namespace Barotrauma if (Enum.TryParse(e.GetAttributeString("type", ""), true, out WearableType type) && type != targetType) { return false; } int headId = e.GetAttributeInt("headid", -1); // if the head id is less than 1, the id is not valid and the condition is ignored. - return headId < 1 || headId == Head.HeadSpriteId; + return headId < 1 || headId == headSpriteId; }); } - public IEnumerable FilterElementsByGenderAndRace(IEnumerable elements) + public IEnumerable FilterElementsByGenderAndRace(IEnumerable elements, Gender gender, Race race) { if (elements == null) { return elements; } return elements.Where(w => - Enum.TryParse(w.GetAttributeString("gender", "None"), true, out Gender g) && g == Head.gender && - Enum.TryParse(w.GetAttributeString("race", "None"), true, out Race r) && r == Head.race); + Enum.TryParse(w.GetAttributeString("gender", "None"), true, out Gender g) && g == gender && + Enum.TryParse(w.GetAttributeString("race", "None"), true, out Race r) && r == race); } private void LoadHeadPresets() @@ -639,7 +697,7 @@ namespace Barotrauma { var wearableElements = Wearables; if (wearableElements == null) { return; } - var wearables = FilterElementsByGenderAndRace(wearableElements).ToList(); + var wearables = FilterElementsByGenderAndRace(wearableElements, head.gender, head.race).ToList(); if (wearables == null) { Head.headSpriteRange = Vector2.Zero; @@ -739,19 +797,19 @@ namespace Barotrauma if (hairs == null) { float commonness = Gender == Gender.Female ? 0.05f : 0.2f; - hairs = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables), WearableType.Hair), WearableType.Hair, commonness); + hairs = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables, head.gender, head.race), WearableType.Hair, head.HeadSpriteId), WearableType.Hair, commonness); } if (beards == null) { - beards = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables), WearableType.Beard), WearableType.Beard); + beards = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables, head.gender, head.race), WearableType.Beard, head.HeadSpriteId), WearableType.Beard); } if (moustaches == null) { - moustaches = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables), WearableType.Moustache), WearableType.Moustache); + moustaches = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables, head.gender, head.race), WearableType.Moustache, head.HeadSpriteId), WearableType.Moustache); } if (faceAttachments == null) { - faceAttachments = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables), WearableType.FaceAttachment), WearableType.FaceAttachment); + faceAttachments = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables, head.gender, head.race), WearableType.FaceAttachment, head.HeadSpriteId), WearableType.FaceAttachment); } if (IsValidIndex(Head.HairIndex, hairs)) @@ -790,49 +848,49 @@ namespace Barotrauma Head.FaceAttachment = GetRandomElement(faceAttachments); Head.FaceAttachmentIndex = faceAttachments.IndexOf(Head.FaceAttachment); } - - static List AddEmpty(IEnumerable elements, WearableType type, float commonness = 1) - { - // Let's add an empty element so that there's a chance that we don't get any actual element -> allows bald and beardless guys, for example. - var emptyElement = new XElement("EmptyWearable", type.ToString(), new XAttribute("commonness", commonness)); - var list = new List() { emptyElement }; - list.AddRange(elements); - return list; - } - - XElement GetRandomElement(IEnumerable elements) - { - var filtered = elements.Where(e => IsWearableAllowed(e)); - if (filtered.Count() == 0) { return null; } - var element = ToolBox.SelectWeightedRandom(filtered.ToList(), GetWeights(filtered).ToList(), Rand.RandSync.Unsynced); - return element == null || element.Name == "Empty" ? null : element; - } - - bool IsWearableAllowed(XElement element) - { - string spriteName = element.Element("sprite").GetAttributeString("name", string.Empty); - return IsAllowed(Head.HairElement, spriteName) && IsAllowed(Head.BeardElement, spriteName) && IsAllowed(Head.MoustacheElement, spriteName) && IsAllowed(Head.FaceAttachment, spriteName); - } - - bool IsAllowed(XElement element, string spriteName) - { - if (element != null) - { - var disallowed = element.GetAttributeStringArray("disallow", new string[0]); - if (disallowed.Any(s => spriteName.Contains(s))) - { - return false; - } - } - return true; - } - - static bool IsValidIndex(int index, List list) => index >= 0 && index < list.Count; - - static IEnumerable GetWeights(IEnumerable elements) => elements.Select(h => h.GetAttributeFloat("commonness", 1f)); } } + private static List AddEmpty(IEnumerable elements, WearableType type, float commonness = 1) + { + // Let's add an empty element so that there's a chance that we don't get any actual element -> allows bald and beardless guys, for example. + var emptyElement = new XElement("EmptyWearable", type.ToString(), new XAttribute("commonness", commonness)); + var list = new List() { emptyElement }; + list.AddRange(elements); + return list; + } + + private XElement GetRandomElement(IEnumerable elements) + { + var filtered = elements.Where(e => IsWearableAllowed(e)); + if (filtered.Count() == 0) { return null; } + var element = ToolBox.SelectWeightedRandom(filtered.ToList(), GetWeights(filtered).ToList(), Rand.RandSync.Unsynced); + return element == null || element.Name == "Empty" ? null : element; + } + + private bool IsWearableAllowed(XElement element) + { + string spriteName = element.Element("sprite").GetAttributeString("name", string.Empty); + return IsAllowed(Head.HairElement, spriteName) && IsAllowed(Head.BeardElement, spriteName) && IsAllowed(Head.MoustacheElement, spriteName) && IsAllowed(Head.FaceAttachment, spriteName); + } + + private bool IsAllowed(XElement element, string spriteName) + { + if (element != null) + { + var disallowed = element.GetAttributeStringArray("disallow", new string[0]); + if (disallowed.Any(s => spriteName.Contains(s))) + { + return false; + } + } + return true; + } + + private static bool IsValidIndex(int index, List list) => index >= 0 && index < list.Count; + + private static IEnumerable GetWeights(IEnumerable elements) => elements.Select(h => h.GetAttributeFloat("commonness", 1f)); + partial void LoadAttachmentSprites(bool omitJob); private int CalculateSalary() @@ -925,72 +983,6 @@ namespace Barotrauma return charElement; } - public void SpawnInventoryItems(Inventory inventory, XElement itemData) - { - SpawnInventoryItemsRecursive(inventory, itemData); - } - - private void SpawnInventoryItemsRecursive(Inventory inventory, XElement element) - { - foreach (XElement itemElement in element.Elements()) - { - var newItem = Item.Load(itemElement, inventory.Owner.Submarine, createNetworkEvent: true); - if (newItem == null) { continue; } - - if (!MathUtils.NearlyEqual(newItem.Condition, newItem.MaxCondition) && - GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) - { - GameMain.NetworkMember.CreateEntityEvent(newItem, new object[] { NetEntityEvent.Type.Status }); - } - - int[] slotIndices = itemElement.GetAttributeIntArray("i", new int[] { 0 }); - if (!slotIndices.Any()) - { - DebugConsole.ThrowError("Invalid inventory data in character \"" + Name + "\" - no slot indices found"); - continue; - } - - //make sure there's no other item in the slot - //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) - { - 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); - } - } - - 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++) - { - if (slotIndices.Contains(i)) - { - inventory.Items[i] = newItem; - } - else if (inventory.Items[i] == newItem) - { - inventory.Items[i] = null; - } - } - - int itemContainerIndex = 0; - var itemContainers = newItem.GetComponents().ToList(); - foreach (XElement childInvElement in itemElement.Elements()) - { - if (itemContainerIndex >= itemContainers.Count) break; - if (!childInvElement.Name.ToString().Equals("inventory", StringComparison.OrdinalIgnoreCase)) { continue; } - SpawnInventoryItemsRecursive(itemContainers[itemContainerIndex].Inventory, childInvElement); - itemContainerIndex++; - } - } - } - public void ApplyHealthData(Character character, XElement healthData) { if (healthData != null) { character?.CharacterHealth.Load(healthData); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs index 90bdbf3ee..8c87f2f5d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs @@ -98,10 +98,11 @@ 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); foreach (Limb limb in character.AnimController.Limbs) { if (limb.IsSevered) { continue; } + if (limb.Hidden) { continue; } float random = Rand.Value(); huskInfection.Clear(); huskInfection.Add(AfflictionPrefab.InternalDamage.Instantiate(random * 10 * deltaTime / limbCount)); @@ -170,7 +171,16 @@ namespace Barotrauma DebugConsole.ThrowError("Failed to turn character \"" + character.Name + "\" into a husk - husk config file not found."); yield return CoroutineStatus.Success; } - var husk = Character.Create(huskedSpeciesName, character.WorldPosition, ToolBox.RandomSeed(8), character.Info, isRemotePlayer: false, hasAi: true); + + XElement parentElement = new XElement("CharacterInfo"); + XElement infoElement = character.Info?.Save(parentElement); + CharacterInfo huskCharacterInfo = infoElement == null ? null : new CharacterInfo(infoElement); + var husk = Character.Create(huskedSpeciesName, character.WorldPosition, ToolBox.RandomSeed(8), huskCharacterInfo, isRemotePlayer: false, hasAi: true); + if (husk.Info != null) + { + husk.Info.Character = husk; + husk.Info.TeamID = Character.TeamType.None; + } foreach (Limb limb in husk.AnimController.Limbs) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index 9512f59a9..d2ffd65ca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -399,7 +399,7 @@ namespace Barotrauma public void ApplyAffliction(Limb targetLimb, Affliction affliction) { - if (Unkillable || Character.GodMode) { return; } + if (!affliction.Prefab.IsBuff && Unkillable || Character.GodMode) { return; } if (affliction.Prefab.LimbSpecific) { if (targetLimb == null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs index 00d102c41..bcfeaa2fe 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/HumanPrefab.cs @@ -61,11 +61,11 @@ namespace Barotrauma } } - [Serialize("None", false)] + [Serialize(CampaignMode.InteractionType.None, false)] public CampaignMode.InteractionType CampaignInteractionType { get; protected set; } - [Serialize("Passive", false)] - public AIObjectiveIdle.BehaviorType BehaviorType { get; protected set; } + [Serialize(AIObjectiveIdle.BehaviorType.Passive, false)] + public AIObjectiveIdle.BehaviorType Behavior { get; protected set; } public List PreferredOutpostModuleTypes { get; protected set; } @@ -163,6 +163,13 @@ namespace Barotrauma { item.AddTag("job:" + job.Name); } + + IdCard idCardComponent = item.GetComponent(); + if (idCardComponent != null) + { + idCardComponent.Initialize(character.Info); + } + var idCardTags = itemElement.GetAttributeStringArray("tags", new string[0]); foreach (string tag in idCardTags) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs index f94bb3bda..a51136f8c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs @@ -197,6 +197,12 @@ namespace Barotrauma item.AddTag("job:" + Name); if (!string.IsNullOrWhiteSpace(spawnPoint.IdCardDesc)) item.Description = spawnPoint.IdCardDesc; + + IdCard idCardComponent = item.GetComponent(); + if (idCardComponent != null) + { + idCardComponent.Initialize(character.Info); + } } foreach (WifiComponent wifiComponent in item.GetComponents()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs index 8818422e2..ab7c508c4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs @@ -90,6 +90,13 @@ namespace Barotrauma private set; } + [Serialize(AIObjectiveIdle.BehaviorType.Passive, false)] + public AIObjectiveIdle.BehaviorType IdleBehavior + { + get; + private set; + } + public string OriginalName { get { return Identifier; } } public ContentPackage ContentPackage { get; private set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index 71ef86a5b..65874e3d5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -223,7 +223,29 @@ namespace Barotrauma public readonly LimbType type; - public readonly bool ignoreCollisions; + private bool ignoreCollisions; + public bool IgnoreCollisions + { + get { return ignoreCollisions; } + set + { + ignoreCollisions = value; + if (body != null) + { + if (ignoreCollisions) + { + body.CollisionCategories = Category.None; + body.CollidesWith = Category.None; + } + else + { + //limbs don't collide with each other + body.CollisionCategories = Physics.CollisionCharacter; + body.CollidesWith = Physics.CollisionAll & ~Physics.CollisionCharacter & ~Physics.CollisionItem & ~Physics.CollisionItemBlocking; + } + } + } + } private bool isSevered; private float severedFadeOutTimer; @@ -310,6 +332,12 @@ namespace Barotrauma public Submarine Submarine => character.Submarine; + public bool Hidden + { + get => Params.Hide; + set => Params.Hide = value; + } + public Vector2 WorldPosition { get { return character.Submarine == null ? Position : Position + character.Submarine.Position; } @@ -549,7 +577,7 @@ namespace Barotrauma { body.CollisionCategories = Category.None; body.CollidesWith = Category.None; - ignoreCollisions = true; + IgnoreCollisions = true; } else { @@ -763,27 +791,56 @@ namespace Barotrauma severedFadeOutTimer = SeveredFadeOutTime; } } + else if (!IsDead) + { + if (Params.BlinkFrequency > 0) + { + if (blinkTimer > -TotalBlinkDurationOut) + { + blinkTimer -= deltaTime; + } + else + { + blinkTimer = Params.BlinkFrequency; + } + } + if (reEnableTimer > 0) + { + reEnableTimer -= deltaTime; + } + else if (reEnableTimer > -1) + { + ReEnable(); + } + } if (attack != null) { attack.UpdateCoolDown(deltaTime); } + } - if (Params.BlinkFrequency > 0) + private float reEnableTimer = -1; + public void HideAndDisable(float duration = 0) + { + Hidden = true; + Disabled = true; + IgnoreCollisions = true; + if (duration > 0) { - if (blinkTimer > -TotalBlinkDurationOut) - { - blinkTimer -= deltaTime; - } - else - { - blinkTimer = Params.BlinkFrequency; - } + reEnableTimer = duration; } } - partial void UpdateProjSpecific(float deltaTime); + private void ReEnable() + { + Hidden = false; + Disabled = false; + IgnoreCollisions = false; + reEnableTimer = -1; + } + partial void UpdateProjSpecific(float deltaTime); private readonly List contactBodies = new List(); /// @@ -942,7 +999,7 @@ namespace Barotrauma #endif if (damageTarget is Character targetCharacter && targetLimb != null) { - attackResult = attack.DoDamageToLimb(character, targetLimb, WorldPosition, 1.0f, playSound); + attackResult = attack.DoDamageToLimb(character, targetLimb, WorldPosition, 1.0f, playSound, body); } else { @@ -952,7 +1009,7 @@ namespace Barotrauma } else { - attackResult = attack.DoDamage(character, damageTarget, WorldPosition, 1.0f, playSound); + attackResult = attack.DoDamage(character, damageTarget, WorldPosition, 1.0f, playSound, body); } } /*if (structureBody != null && attack.StickChance > Rand.Range(0.0f, 1.0f, Rand.RandSync.Server)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index 4b5530063..a76ecdf96 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -492,8 +492,6 @@ namespace Barotrauma [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; } - // TODO: latchonto, swarming - public IEnumerable Targets => targets; protected readonly List targets = new List(); @@ -589,6 +587,21 @@ namespace Barotrauma [Serialize(false, true, description: "Should the target be ignored if it's inside a container/inventory. Only affects items."), Editable] 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; } + + [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; } + + [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; } + + [Serialize(10f, true, description: "How much the sweep affects the steering?"), Editable(MinValueFloat = 0, MaxValueFloat = 100, ValueStep = 1f, DecimalCount = 1)] + public float SweepStrength { get; private set; } + + [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; } + public TargetParams(XElement element, CharacterParams character) : base(element, character) { } public TargetParams(string tag, AIState state, float priority, CharacterParams character) : base(CreateNewElement(tag, state, priority), character) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs index 9a02bf2a8..fc0d4fe22 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs @@ -745,7 +745,7 @@ namespace Barotrauma { if (LightSource != null) { return false; } var lightSourceElement = new XElement("lightsource", - new XElement("lighttexture", new XAttribute("texture", "Content/Lights/light.png"))); + new XElement("lighttexture", new XAttribute("texture", "Content/Lights/pointlight_bright.png"))); TryAddSubParam(lightSourceElement, (e, c) => new LightSourceParams(e, c), out LightSourceParams newLightSource); LightSource = newLightSource; return LightSource != null; diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs index 35e0bead6..d8c8483b2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs @@ -21,6 +21,7 @@ namespace Barotrauma Outpost, OutpostModule, OutpostConfig, + BeaconStation, NPCSets, Factions, Text, @@ -28,6 +29,7 @@ namespace Barotrauma LocationTypes, MapGenerationParameters, LevelGenerationParameters, + CaveGenerationParameters, LevelObjectPrefabs, RandomEvents, Missions, @@ -47,7 +49,8 @@ namespace Barotrauma Wreck, Corpses, WreckAIConfig, - UpgradeModules + UpgradeModules, + MapCreature } public class ContentPackage @@ -84,6 +87,7 @@ namespace Barotrauma ContentType.Factions, ContentType.MapGenerationParameters, ContentType.LevelGenerationParameters, + ContentType.CaveGenerationParameters, ContentType.Missions, ContentType.LevelObjectPrefabs, ContentType.RuinConfig, @@ -92,10 +96,12 @@ namespace Barotrauma ContentType.OutpostConfig, ContentType.Wreck, ContentType.WreckAIConfig, + ContentType.BeaconStation, ContentType.Afflictions, ContentType.Orders, ContentType.Corpses, - ContentType.UpgradeModules + ContentType.UpgradeModules, + ContentType.MapCreature }; //at least one file of each these types is required in core content packages @@ -111,11 +117,13 @@ namespace Barotrauma ContentType.Factions, ContentType.Wreck, ContentType.WreckAIConfig, + ContentType.BeaconStation, ContentType.Text, ContentType.ServerExecutable, ContentType.LocationTypes, ContentType.MapGenerationParameters, ContentType.LevelGenerationParameters, + ContentType.CaveGenerationParameters, ContentType.RandomEvents, ContentType.Missions, ContentType.RuinConfig, @@ -384,6 +392,7 @@ namespace Barotrauma case ContentType.OutpostModule: case ContentType.Submarine: case ContentType.Wreck: + case ContentType.BeaconStation: break; default: try diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 73298718e..c928da73d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -11,6 +11,7 @@ using System.Globalization; using Barotrauma.IO; using System.Linq; using System.Text; +using Barotrauma.MapCreatures.Behavior; namespace Barotrauma { @@ -732,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); @@ -815,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) => { @@ -1160,8 +1166,15 @@ namespace Barotrauma { foreach (Character c in Character.CharacterList) { - if (!(c.AIController is EnemyAIController)) continue; - c.SetAllDamage(200.0f, 0.0f, 0.0f); + if (c.AIController is EnemyAIController enemyAI && enemyAI.PetBehavior == null) + { + c.SetAllDamage(200.0f, 0.0f, 0.0f); + } + } + + foreach (Hull hull in Hull.hullList) + { + hull.BallastFlora?.Kill(); } }, null, isCheat: true)); @@ -1259,6 +1272,75 @@ namespace Barotrauma } } }, isCheat: true)); + + commands.Add(new Command("ballastflora", "infectballast [options]: Infect ballasts and control its growth.", args => + { + if (args.Length == 0) + { + ThrowError("No action specified."); + return; + } + + string primaryAction = args.Length > 0 ? args[0] : ""; + string secondaryArgument = args.Length > 1 ? args[1] : ""; + + if (Submarine.MainSub == null) + { + ThrowError("No submarine loaded."); + return; + } + + if (primaryAction.Equals("infect", StringComparison.OrdinalIgnoreCase)) + { + List pumps = new List(); + foreach (Item item in Submarine.MainSub.GetItems(true)) + { + if (item.CurrentHull != null && item.HasTag("ballast") && item.GetComponent() is { } pump) + { + pumps.Add(pump); + } + } + + if (pumps.Any()) + { + BallastFloraPrefab prefab = string.IsNullOrWhiteSpace(secondaryArgument) ? BallastFloraPrefab.Prefabs.First() : BallastFloraPrefab.Find(secondaryArgument); + if (prefab == null) + { + ThrowError($"No such behavior: {secondaryArgument}"); + return; + } + + Pump random = pumps.GetRandom(); + random.InfectBallast(prefab.Identifier); + NewMessage($"Infected {random.Name} with {prefab.Identifier}.", Color.Green); + return; + } + + ThrowError("No available pumps to infect on this submarine."); + } + + if (primaryAction.Equals("growthwarp", StringComparison.OrdinalIgnoreCase)) + { + if (int.TryParse(secondaryArgument, out int value)) + { + foreach (Hull hull in Hull.hullList.Where(h => h.BallastFlora != null)) + { + BallastFloraBehavior bs = hull.BallastFlora; + bs.GrowthWarps = value; + } + + NewMessage("Accelerating growth...", Color.Green); + return; + } + + ThrowError($"Invalid integer \"{secondaryArgument}\"."); + } + }, isCheat: true, getValidArgs: () => + { + string[] primaries = { "infect", "growthwarp" }; + string[] identifiers = BallastFloraPrefab.Prefabs.Select(bfp => bfp.Identifier).Distinct().ToArray(); + return new[] { primaries, identifiers }; + })); commands.Add(new Command("difficulty|leveldifficulty", "difficulty [0-100]: Change the level difficulty setting in the server lobby.", null)); @@ -1899,13 +1981,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 7aaf5d4f7..6d0d61c77 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs @@ -56,7 +56,9 @@ namespace Barotrauma public override void Init(bool affectSubImmediately) { spawnPos = Level.Loaded.GetRandomItemPos( - (Rand.Value(Rand.RandSync.Server) < 0.5f) ? Level.PositionType.MainPath : Level.PositionType.Cave | Level.PositionType.Ruin, + (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); spawnPending = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs index 3b7f21e7c..d6bd7d329 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Event.cs @@ -56,5 +56,10 @@ namespace Barotrauma { return true; } + + public virtual bool LevelMeetsRequirements() + { + return true; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SkillCheckAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SkillCheckAction.cs index 5f3a46771..d63601890 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SkillCheckAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SkillCheckAction.cs @@ -13,6 +13,9 @@ namespace Barotrauma [Serialize(0.0f, true)] public float RequiredLevel { get; set; } + [Serialize(true, true)] + public bool ProbabilityBased { get; set; } + [Serialize("", true)] public string TargetTag { get; set; } @@ -27,7 +30,15 @@ namespace Barotrauma protected override bool? DetermineSuccess() { var potentialTargets = ParentEvent.GetTargets(TargetTag).Where(e => e is Character).Select(e => e as Character); - return potentialTargets.Any(chr => chr.GetSkillLevel(RequiredSkill?.ToLowerInvariant()) >= RequiredLevel); + + if (ProbabilityBased) + { + return potentialTargets.Any(chr => chr.GetSkillLevel(RequiredSkill?.ToLowerInvariant()) / RequiredLevel > Rand.Range(0.0f, 1.0f, Rand.RandSync.Unsynced)); + } + else + { + return potentialTargets.Any(chr => chr.GetSkillLevel(RequiredSkill?.ToLowerInvariant()) >= RequiredLevel); + } } public override string ToDebugString() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs index 4ba47c503..16b62c25b 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; @@ -123,7 +126,7 @@ namespace Barotrauma var idleObjective = humanAI.ObjectiveManager.GetObjective(); if (idleObjective != null) { - idleObjective.Behavior = humanPrefab.BehaviorType; + idleObjective.Behavior = humanPrefab.Behavior; foreach (string moduleType in humanPrefab.PreferredOutpostModuleTypes) { idleObjective.PreferredOutpostModuleTypes.Add(moduleType); @@ -202,6 +205,7 @@ namespace Barotrauma ParentEvent.AddTarget(TargetTag, newItem); } spawnedEntity = newItem; + newItem?.SetIgnoreByAI(IgnoreByAI); } } } 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/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 28f60d666..98ff583d4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -107,7 +107,13 @@ namespace Barotrauma if (initialEventSet != null) { pendingEventSets.Add(initialEventSet); - CreateEvents(initialEventSet); + int seed = ToolBox.StringToInt(level.Seed); + foreach (var previousEvent in level.LevelData.EventHistory) + { + seed ^= ToolBox.StringToInt(previousEvent.Identifier); + } + MTRandom rand = new MTRandom(seed); + CreateEvents(initialEventSet, rand); } if (level?.LevelData?.Type == LevelData.LevelType.Outpost) @@ -325,7 +331,7 @@ namespace Barotrauma return retVal; } - private void CreateEvents(EventSet eventSet) + private void CreateEvents(EventSet eventSet, Random rand) { if (level == null) { return; } int applyCount = 1; @@ -343,13 +349,6 @@ namespace Barotrauma { if (eventSet.EventPrefabs.Count > 0) { - int seed = ToolBox.StringToInt(level.Seed); - foreach (var previousEvent in level.LevelData.EventHistory) - { - seed |= ToolBox.StringToInt(previousEvent.Identifier); - } - - MTRandom rand = new MTRandom(seed); List> unusedEvents = new List>(eventSet.EventPrefabs); for (int j = 0; j < eventSet.EventCount; j++) { @@ -357,6 +356,7 @@ namespace Barotrauma if (eventPrefab != null) { var newEvent = eventPrefab.First.CreateInstance(); + if (newEvent == null) { continue; } newEvent.Init(true); DebugConsole.Log("Initialized event " + newEvent.ToString()); if (!selectedEvents.ContainsKey(eventSet)) @@ -371,7 +371,7 @@ namespace Barotrauma if (eventSet.ChildSets.Count > 0) { var newEventSet = SelectRandomEvents(eventSet.ChildSets); - if (newEventSet != null) { CreateEvents(newEventSet); } + if (newEventSet != null) { CreateEvents(newEventSet, rand); } } } else @@ -379,6 +379,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)) @@ -390,7 +391,7 @@ namespace Barotrauma foreach (EventSet childEventSet in eventSet.ChildSets) { - CreateEvents(childEventSet); + CreateEvents(childEventSet, rand); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs index 6c64271be..e0fc0e179 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventPrefab.cs @@ -50,6 +50,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/Missions/BeaconMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs new file mode 100644 index 000000000..ebe2d27c0 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs @@ -0,0 +1,123 @@ +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + partial class BeaconMission : Mission + { + private bool swarmSpawned; + private readonly string monsterSpeciesName; + private Point monsterCountRange; + private Level level; + private readonly string sonarLabel; + + public BeaconMission(MissionPrefab prefab, Location[] locations) : base(prefab, locations) + { + swarmSpawned = false; + + XElement monsterElement = prefab.ConfigElement.Element("monster"); + + monsterSpeciesName = monsterElement.GetAttributeString("character", string.Empty); + int defaultCount = monsterElement.GetAttributeInt("count", -1); + if (defaultCount < 0) + { + defaultCount = monsterElement.GetAttributeInt("amount", 1); + } + int min = Math.Min(monsterElement.GetAttributeInt("min", defaultCount), 255); + int max = Math.Min(Math.Max(min, monsterElement.GetAttributeInt("max", defaultCount)), 255); + + monsterCountRange = new Point(min, max); + + sonarLabel = TextManager.Get("beaconstationsonarlabel"); + } + + public override string SonarLabel + { + get + { + return string.IsNullOrEmpty(base.SonarLabel) ? sonarLabel : base.SonarLabel; + } + } + + public override IEnumerable SonarPositions + { + get + { + 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++) + { + 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; + } + } + + public override void End() + { + completed = level.CheckBeaconActive(); + if (completed) + { + ChangeLocationType("None", "Explored"); + GiveReward(); + } + } + + public override void AdjustLevelData(LevelData levelData) + { + levelData.HasBeaconStation = true; + levelData.IsBeaconActive = false; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs new file mode 100644 index 000000000..7a8e774e4 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs @@ -0,0 +1,208 @@ +using Barotrauma.Extensions; +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + partial class MineralMission : Mission + { + private Dictionary> ResourceClusters { get; } = new Dictionary>(); + private Dictionary> SpawnedResources { get; } = new Dictionary>(); + private Dictionary RelevantLevelResources { get; } = new Dictionary(); + private List> MissionClusterPositions { get; } = new List>(); + + public override IEnumerable SonarPositions + { + get + { + return MissionClusterPositions + .Where(p => SpawnedResources.ContainsKey(p.Item1) && AnyAreUncollected(SpawnedResources[p.Item1])) + .Select(p => p.Item2); + } + } + + public MineralMission(MissionPrefab prefab, Location[] locations) : base(prefab, locations) + { + var configElement = prefab.ConfigElement.Element("Items"); + foreach (var c in configElement.GetChildElements("Item")) + { + var identifier = c.GetAttributeString("identifier", null); + if (string.IsNullOrWhiteSpace(identifier)) { continue; } + if (ResourceClusters.ContainsKey(identifier)) + { + ResourceClusters[identifier].First++; + } + else + { + ResourceClusters.Add(identifier, new Pair(1, 0.0f)); + } + } + } + + public override void Start(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 + } + + if (IsClient) { return; } + foreach (var kvp in ResourceClusters) + { + var prefab = ItemPrefab.Find(null, kvp.Key); + 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; + } + CalculateMissionClusterPositions(); + FindRelevantLevelResources(); + } + + public override void Update(float deltaTime) + { + if (IsClient) { return; } + switch (State) + { + case 0: + if (!EnoughHaveBeenCollected()) { return; } + State = 1; + break; + case 1: + if (!Submarine.MainSub.AtEndPosition && !Submarine.MainSub.AtStartPosition) { return; } + State = 2; + break; + } + } + + public override void End() + { + if (EnoughHaveBeenCollected()) + { + 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() + { + RelevantLevelResources.Clear(); + foreach (var identifier in ResourceClusters.Keys) + { + var items = Item.ItemList.Where(i => i.Prefab.Identifier == identifier && + i.Submarine == null && i.ParentInventory == null && + (!(i.GetComponent() is Holdable h) || (h.Attachable && h.Attached))) + .ToArray(); + RelevantLevelResources.Add(identifier, items); + } + } + + private bool EnoughHaveBeenCollected() + { + foreach (var kvp in ResourceClusters) + { + if (RelevantLevelResources.TryGetValue(kvp.Key, out var availableResources)) + { + var collected = availableResources.Count(r => HasBeenCollected(r)); + var needed = kvp.Value.First; + if (collected < needed) { return false; } + } + else + { + return false; + } + } + return true; + } + + private bool HasBeenCollected(Item item) + { + if (item == null) { return false; } + if (item.Removed) { return false; } + var owner = item.GetRootInventoryOwner(); + if (owner.Submarine != null && owner.Submarine.Info.Type == SubmarineType.Player) + { + return true; + } + else if (owner is Character c) + { + return c.Info != null && GameMain.GameSession.CrewManager.CharacterInfos.Contains(c.Info); + } + return false; + } + + private bool AnyAreUncollected(IEnumerable items) + => items.Any(i => !HasBeenCollected(i)); + + private void CalculateMissionClusterPositions() + { + MissionClusterPositions.Clear(); + foreach (var kvp in SpawnedResources) + { + if (kvp.Value.None()) { continue; } + var pos = Vector2.Zero; + var itemCount = 0; + foreach (var i in kvp.Value.Where(i => i != null && !i.Removed)) + { + pos += i.WorldPosition; + itemCount++; + } + pos /= itemCount; + MissionClusterPositions.Add(new Tuple(kvp.Key, pos)); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index 2901498fa..31d5a4d21 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -95,7 +95,7 @@ namespace Barotrauma get { return Enumerable.Empty(); } } - public string SonarLabel + public virtual string SonarLabel { get { return Prefab.SonarLabel; } } @@ -233,5 +233,17 @@ namespace Barotrauma } } } + + protected void ChangeLocationType(string from, string to) + { + if (GameMain.GameSession.GameMode is CampaignMode && !IsClient) + { + int srcIndex = Locations[0].Type.Identifier.Equals(from, StringComparison.OrdinalIgnoreCase) ? 0 : 1; + var upgradeLocation = Locations[srcIndex]; + upgradeLocation.ChangeType(LocationType.List.Find(lt => lt.Identifier.Equals(to, StringComparison.OrdinalIgnoreCase))); + } + } + + 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 f9d59ba4d..63cb3de8b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs @@ -14,20 +14,29 @@ namespace Barotrauma Salvage = 0x1, Monster = 0x2, Cargo = 0x4, - Combat = 0x8, - All = 0xf + Beacon = 0x8, + Nest = 0x10, + Mineral = 0x20, + Combat = 0x40, + All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat } partial class MissionPrefab { public static readonly List List = new List(); - private static readonly Dictionary missionClasses = new Dictionary() + public static readonly Dictionary CoOpMissionClasses = new Dictionary() { { MissionType.Salvage, typeof(SalvageMission) }, { MissionType.Monster, typeof(MonsterMission) }, { MissionType.Cargo, typeof(CargoMission) }, - { MissionType.Combat, typeof(CombatMission) }, + { MissionType.Beacon, typeof(BeaconMission) }, + { MissionType.Nest, typeof(NestMission) }, + { MissionType.Mineral, typeof(MineralMission) }, + }; + public static readonly Dictionary PvPMissionClasses = new Dictionary() + { + { MissionType.Combat, typeof(CombatMission) } }; private readonly ConstructorInfo constructor; @@ -146,15 +155,32 @@ namespace Barotrauma Headers = new List(); Messages = new List(); AllowedLocationTypes = new List>(); + + for (int i = 0; i < 100; i++) + { + string header = TextManager.Get("MissionHeader" + i + "." + TextIdentifier, true); + string message = TextManager.Get("MissionMessage" + i + "." + TextIdentifier, true); + if (!string.IsNullOrEmpty(message)) + { + Headers.Add(header); + Messages.Add(message); + } + } + + int messageIndex = 0; foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "message": - int index = Messages.Count; - - Headers.Add(TextManager.Get("MissionHeader" + index + "." + TextIdentifier, true) ?? subElement.GetAttributeString("header", "")); - Messages.Add(TextManager.Get("MissionMessage" + index + "." + TextIdentifier, true) ?? subElement.GetAttributeString("text", "")); + if (messageIndex > Headers.Count - 1) + { + Headers.Add(string.Empty); + Messages.Add(string.Empty); + } + Headers[messageIndex] = TextManager.Get("MissionHeader" + messageIndex + "." + TextIdentifier, true) ?? subElement.GetAttributeString("header", ""); + Messages[messageIndex] = TextManager.Get("MissionMessage" + messageIndex + "." + TextIdentifier, true) ?? subElement.GetAttributeString("text", ""); + messageIndex++; break; case "locationtype": AllowedLocationTypes.Add(new Pair( @@ -211,7 +237,18 @@ namespace Barotrauma return; } - constructor = missionClasses[Type].GetConstructor(new[] { typeof(MissionPrefab), typeof(Location[]) }); + if (CoOpMissionClasses.ContainsKey(Type)) + { + constructor = CoOpMissionClasses[Type].GetConstructor(new[] { typeof(MissionPrefab), typeof(Location[]) }); + } + else if (PvPMissionClasses.ContainsKey(Type)) + { + constructor = PvPMissionClasses[Type].GetConstructor(new[] { typeof(MissionPrefab), typeof(Location[]) }); + } + else + { + DebugConsole.ThrowError("Error in mission prefab \"" + Name + "\" - unsupported mission type \"" + Type.ToString() + "\""); + } InitProjSpecific(element); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs index 6fb7534ba..79088e8cb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs @@ -86,17 +86,27 @@ namespace Barotrauma { 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.Loaded.Size.X * 0.3f, out Vector2 spawnPos); + Level.Loaded.TryGetInterestingPosition(true, Level.PositionType.MainPath | Level.PositionType.SidePath, 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); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs new file mode 100644 index 000000000..331be9b33 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs @@ -0,0 +1,294 @@ +using Barotrauma.Extensions; +using FarseerPhysics; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using Voronoi2; + +namespace Barotrauma +{ + partial class NestMission : Mission + { + private readonly XElement itemConfig; + private readonly List items = new List(); + private readonly Dictionary statusEffectOnApproach = new Dictionary(); + + //string = filename, point = min,max + private readonly HashSet> monsterPrefabs = new HashSet>(); + + private readonly float itemSpawnRadius = 800.0f; + private readonly float approachItemsRadius = 1000.0f; + private readonly float monsterSpawnRadius = 3000.0f; + + private readonly bool requireDelivery; + + private readonly Level.PositionType spawnPositionType; + + private Vector2 nestPosition; + + + public override IEnumerable SonarPositions + { + get + { + if (State > 0) + { + Enumerable.Empty(); + } + else + { + yield return nestPosition; + } + } + } + + public NestMission(MissionPrefab prefab, Location[] locations) + : base(prefab, locations) + { + itemConfig = prefab.ConfigElement.Element("Items"); + + itemSpawnRadius = prefab.ConfigElement.GetAttributeFloat("itemspawnradius", 800.0f); + approachItemsRadius = prefab.ConfigElement.GetAttributeFloat("approachitemsradius", itemSpawnRadius * 2.0f); + monsterSpawnRadius = prefab.ConfigElement.GetAttributeFloat("monsterspawnradius", approachItemsRadius * 2.0f); + + requireDelivery = prefab.ConfigElement.GetAttributeBool("requiredelivery", false); + + string spawnPositionTypeStr = prefab.ConfigElement.GetAttributeString("spawntype", ""); + if (string.IsNullOrWhiteSpace(spawnPositionTypeStr) || + !Enum.TryParse(spawnPositionTypeStr, true, out spawnPositionType)) + { + spawnPositionType = Level.PositionType.Cave | Level.PositionType.Ruin; + } + + + foreach (var monsterElement in prefab.ConfigElement.GetChildElements("monster")) + { + string speciesName = monsterElement.GetAttributeString("character", string.Empty); + int defaultCount = monsterElement.GetAttributeInt("count", -1); + if (defaultCount < 0) + { + defaultCount = monsterElement.GetAttributeInt("amount", 1); + } + int min = Math.Min(monsterElement.GetAttributeInt("min", defaultCount), 255); + int max = Math.Min(Math.Max(min, monsterElement.GetAttributeInt("max", defaultCount)), 255); + var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName); + if (characterPrefab != null) + { + monsterPrefabs.Add(new Tuple(characterPrefab, new Point(min, max))); + } + else + { + DebugConsole.ThrowError($"Error in monster mission \"{prefab.Identifier}\". Could not find a character prefab with the name \"{speciesName}\"."); + } + } + + } + + public override void Start(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 + float minDistance = spawnPositionType == Level.PositionType.Ruin || spawnPositionType == Level.PositionType.Cave || spawnPositionType == Level.PositionType.Wreck ? + 0.0f : Level.Loaded.Size.X * 0.3f; + nestPosition = Level.Loaded.GetRandomItemPos(spawnPositionType, 100.0f, minDistance, 30.0f); + List spawnEdges = new List(); + if (spawnPositionType == Level.PositionType.Cave) + { + var nearbyCells = Level.Loaded.GetCells(nestPosition, searchDepth: 3); + if (nearbyCells.Any()) + { + List validEdges = new List(); + foreach (var edge in nearbyCells.SelectMany(c => c.Edges)) + { + if (!edge.NextToCave || !edge.IsSolid) { continue; } + if (Level.Loaded.ExtraWalls.Any(w => w.IsPointInside(edge.Center + edge.GetNormal(edge.Cell1 ?? edge.Cell2) * 100.0f))) { continue; } + validEdges.Add(edge); + } + + if (validEdges.Any()) + { + spawnEdges.AddRange(validEdges.Where(e => MathUtils.LineSegmentToPointDistanceSquared(e.Point1.ToPoint(), e.Point2.ToPoint(), nestPosition.ToPoint()) < itemSpawnRadius * itemSpawnRadius).Distinct()); + } + //no valid edges found close enough to the nest position, find the closest one + if (!spawnEdges.Any()) + { + GraphEdge closestEdge = null; + float closestDist = float.PositiveInfinity; + foreach (var edge in nearbyCells.SelectMany(c => c.Edges)) + { + if (!edge.NextToCave || !edge.IsSolid) { continue; } + float dist = Vector2.DistanceSquared(edge.Center, nestPosition); + if (dist < closestDist) + { + closestEdge = edge; + closestDist = dist; + } + } + if (closestEdge != null) + { + spawnEdges.Add(closestEdge); + } + } + } + } + + foreach (XElement subElement in itemConfig.Elements()) + { + string itemIdentifier = subElement.GetAttributeString("identifier", ""); + if (!(MapEntityPrefab.Find(null, itemIdentifier) is ItemPrefab itemPrefab)) + { + DebugConsole.ThrowError("Couldn't spawn item for nest mission: item prefab \"" + itemIdentifier + "\" not found"); + continue; + } + + Vector2 spawnPos = nestPosition; + float rotation = 0.0f; + if (spawnEdges.Any()) + { + var edge = spawnEdges.GetRandom(Rand.RandSync.Server); + spawnPos = Vector2.Lerp(edge.Point1, edge.Point2, Rand.Range(0.1f, 0.9f, Rand.RandSync.Server)); + Vector2 normal = Vector2.UnitY; + if (edge.Cell1 != null && edge.Cell1.CellType == CellType.Solid) + { + normal = edge.GetNormal(edge.Cell1); + } + else if (edge.Cell2 != null && edge.Cell2.CellType == CellType.Solid) + { + normal = edge.GetNormal(edge.Cell2); + } + spawnPos += normal * 10.0f; + rotation = MathUtils.VectorToAngle(normal) - MathHelper.PiOver2; + } + + var item = new Item(itemPrefab, spawnPos, null); + item.body.FarseerBody.BodyType = BodyType.Kinematic; + item.body.SetTransformIgnoreContacts(item.body.SimPosition, rotation); + item.FindHull(); + items.Add(item); + + var statusEffectElement = subElement.Element("StatusEffectOnApproach") ?? subElement.Element("statuseffectonapproach"); + if (statusEffectElement != null) + { + statusEffectOnApproach.Add(item, StatusEffect.Load(statusEffectElement, Prefab.Identifier)); + } + } + } + } + + public override void Update(float deltaTime) + { + if (IsClient) + { + foreach (Item item in items) + { + if (item.ParentInventory != null && item.body != null) { item.body.FarseerBody.BodyType = BodyType.Dynamic; } + } + return; + } + switch (State) + { + case 0: + foreach (Item item in items) + { + if (item.ParentInventory != null && item.body != null) { item.body.FarseerBody.BodyType = BodyType.Dynamic; } + if (statusEffectOnApproach.ContainsKey(item)) + { + foreach (Character character in Character.CharacterList) + { + if (character.IsPlayer && Vector2.DistanceSquared(nestPosition, character.WorldPosition) < approachItemsRadius * approachItemsRadius) + { + statusEffectOnApproach[item].Apply(statusEffectOnApproach[item].type, 1.0f, item, item); + statusEffectOnApproach.Remove(item); + break; + } + } + } + } + if (monsterPrefabs.Any()) + { + foreach (Character character in Character.CharacterList) + { + if (character.IsPlayer && Vector2.DistanceSquared(nestPosition, character.WorldPosition) < monsterSpawnRadius * monsterSpawnRadius) + { + foreach (var monster in monsterPrefabs) + { + int amount = Rand.Range(monster.Item2.X, monster.Item2.Y + 1); + for (int i = 0; i < amount; i++) + { + Character.Create(monster.Item1.Identifier, nestPosition + Rand.Vector(100.0f), ToolBox.RandomSeed(8), createNetworkEvent: true); + } + } + monsterPrefabs.Clear(); + break; + } + } + } + + //continue when all items are in the sub or destroyed + if (AllItemsDestroyedOrRetrieved()) { State = 1; } + + break; + case 1: + if (!Submarine.MainSub.AtEndPosition && !Submarine.MainSub.AtStartPosition) { return; } + State = 2; + break; + } + } + + private bool AllItemsDestroyedOrRetrieved() + { + if (requireDelivery) + { + foreach (Item item in items) + { + Submarine parentSub = item.CurrentHull?.Submarine ?? item.GetRootInventoryOwner()?.Submarine; + if (parentSub?.Info?.Type == SubmarineType.Player) { continue; } + return false; + } + } + else + { + foreach (Item item in items) + { + if (item.Removed || item.Condition <= 0.0f) { continue; } + if (Vector2.Distance(item.WorldPosition, nestPosition) > Math.Max(itemSpawnRadius * 2, 3000.0f)) { continue; } + Submarine parentSub = item.CurrentHull?.Submarine ?? item.GetRootInventoryOwner()?.Submarine; + if (parentSub?.Info?.Type == SubmarineType.Player) { continue; } + return false; + } + } + return true; + } + + public override void End() + { + if (AllItemsDestroyedOrRetrieved()) + { + GiveReward(); + completed = true; + } + foreach (Item item in items) + { + if (item != null && !item.Removed) + { + item.Remove(); + } + } + items.Clear(); + failed = !completed && state > 0; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs index c8e802ad1..defadaf0a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs @@ -109,8 +109,8 @@ namespace Barotrauma item = null; if (!IsClient) { - //ruin/wreck items are allowed to spawn close to the sub - float minDistance = spawnPositionType == Level.PositionType.Ruin || spawnPositionType == Level.PositionType.Wreck ? + //ruin/cave/wreck items are allowed to spawn close to the sub + float minDistance = spawnPositionType == Level.PositionType.Ruin || spawnPositionType == Level.PositionType.Cave || spawnPositionType == Level.PositionType.Wreck ? 0.0f : Level.Loaded.Size.X * 0.3f; Vector2 position = Level.Loaded.GetRandomItemPos(spawnPositionType, 100.0f, minDistance, 30.0f); @@ -121,6 +121,7 @@ namespace Barotrauma { case Level.PositionType.Cave: case Level.PositionType.MainPath: + case Level.PositionType.SidePath: item = suitableItems.FirstOrDefault(it => Vector2.DistanceSquared(it.WorldPosition, position) < 1000.0f); break; case Level.PositionType.Ruin: diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs index 79c965621..007b6ddbf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs @@ -149,8 +149,12 @@ namespace Barotrauma continue; } } - if (position.PositionType != Level.PositionType.MainPath) { continue; } - if (Level.Loaded.ExtraWalls.Any(w => w.Cells.Any(c => c.IsPointInside(position.Position.ToVector2())))) + if (position.PositionType != Level.PositionType.MainPath && + position.PositionType != Level.PositionType.SidePath) + { + continue; + } + if (Level.Loaded.ExtraWalls.Any(w => w.IsPointInside(position.Position.ToVector2()))) { removals.Add(position); } @@ -281,7 +285,8 @@ namespace Barotrauma spawnPos = spawnPoint.WorldPosition; } } - else if (chosenPosition.PositionType == Level.PositionType.MainPath && offset > 0) + else if ((chosenPosition.PositionType == Level.PositionType.MainPath || chosenPosition.PositionType == Level.PositionType.SidePath) + && offset > 0) { Vector2 dir; var waypoints = WayPoint.WayPointList.FindAll(wp => wp.Submarine == null); @@ -381,9 +386,10 @@ namespace Barotrauma //+1 because Range returns an integer less than the max value int amount = Rand.Range(minAmount, maxAmount + 1); monsters = new List(); - float offsetAmount = spawnPosType == Level.PositionType.MainPath ? scatter : 100; + float offsetAmount = spawnPosType == Level.PositionType.MainPath || spawnPosType == Level.PositionType.SidePath ? scatter : 100; for (int i = 0; i < amount; i++) { + string seed = Level.Loaded.Seed + i.ToString(); CoroutineManager.InvokeAfter(() => { //round ended before the coroutine finished @@ -392,7 +398,7 @@ namespace Barotrauma System.Diagnostics.Debug.Assert(GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer, "Clients should not create monster events."); Vector2 pos = spawnPos.Value + Rand.Vector(offsetAmount); - if (spawnPosType == Level.PositionType.MainPath) + if (spawnPosType == Level.PositionType.MainPath || spawnPosType == Level.PositionType.SidePath) { if (Submarine.Loaded.Any(s => ToolBox.GetWorldBounds(s.Borders.Center, s.Borders.Size).ContainsWorld(pos))) { @@ -406,7 +412,7 @@ namespace Barotrauma } } - monsters.Add(Character.Create(speciesName, pos, Level.Loaded.Seed + i.ToString(), null, false, true, true)); + monsters.Add(Character.Create(speciesName, pos, seed, characterInfo: null, isRemotePlayer: false, hasAi: true, createNetworkEvent: true)); if (monsters.Count == amount) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs index c97e728c3..c638f057d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs @@ -13,6 +13,8 @@ namespace Barotrauma private int prevEntityCount; private int prevPlayerCount, prevBotCount; + private string[] requiredDestinationTypes; + public int CurrentActionIndex { get; private set; } public List Actions { get; } = new List(); public Dictionary> Targets { get; } = new Dictionary>(); @@ -39,6 +41,8 @@ namespace Barotrauma { DebugConsole.ThrowError($"Scripted event \"{prefab.Identifier}\" has no actions. The event will do nothing."); } + + requiredDestinationTypes = prefab.ConfigElement.GetAttributeStringArray("requireddestinationtypes", null); } public void AddTarget(string tag, Entity target) @@ -199,5 +203,14 @@ namespace Barotrauma currentAction.Update(deltaTime); } } + + public override bool LevelMeetsRequirements() + { + if (requiredDestinationTypes == null) { return true; } + var currLocation = GameMain.GameSession?.Campaign?.Map.CurrentLocation; + if (currLocation == null) { return true; } + var locations = currLocation?.Connections?.Select(c => c.Locations.First(l => l != currLocation)); + return locations.Any(l => requiredDestinationTypes.Any(t => l.Type.Identifier.Equals(t, StringComparison.OrdinalIgnoreCase))); + } } } 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 6cd5d2d46..004bb2a65 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ForbiddenWordFilter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ForbiddenWordFilter.cs @@ -48,7 +48,7 @@ namespace Barotrauma foreach (string word in words) { - if (forbiddenWords.Any(w => Homoglyphs.Compare(word, w) || Homoglyphs.Compare(word + 's', w))) + if (forbiddenWords.Any(w => Homoglyphs.Compare(word, w))) { forbiddenWord = word; return true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs index e8cc051b1..089d9f5b0 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()); } } @@ -204,7 +205,7 @@ namespace Barotrauma { SpawnedInOutpost = validContainer.Key.Item.SpawnedInOutpost, OriginalModuleIndex = validContainer.Key.Item.OriginalModuleIndex, - OriginalContainerID = validContainer.Key.Item.OriginalID + OriginalContainerID = validContainer.Key.Item.ID }; foreach (WifiComponent wifiComponent in item.GetComponents()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs index 8a30b0860..d231abb53 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs @@ -217,7 +217,7 @@ namespace Barotrauma if (containerPrefab == null) { - DebugConsole.ThrowError("Cargo spawning failed - could not find the item prefab for container \"" + containerPrefab.Name + "\"!"); + DebugConsole.ThrowError("Cargo spawning failed - could not find the item prefab for container \"" + pi.ItemPrefab.CargoContainerIdentifier + "\"!"); continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs index d4c4a0e27..e236209b0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs @@ -25,9 +25,11 @@ namespace Barotrauma public bool HasBots { get; set; } - public List> ActiveOrders { get; } = new List>(); + public List> ActiveOrders { get; } = new List>(); public bool IsSinglePlayer { get; private set; } + public ReadyCheck ActiveReadyCheck; + public CrewManager(bool isSinglePlayer) { IsSinglePlayer = isSinglePlayer; @@ -38,7 +40,7 @@ namespace Barotrauma partial void InitProjectSpecific(); - public bool AddOrder(Order order, float fadeOutTime) + public bool AddOrder(Order order, float? fadeOutTime) { if (order.TargetEntity == null) { @@ -46,7 +48,10 @@ namespace Barotrauma return false; } - Pair existingOrder = ActiveOrders.Find(o => o.First.Prefab == order.Prefab && o.First.TargetEntity == order.TargetEntity); + 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)); + if (existingOrder != null) { existingOrder.Second = fadeOutTime; @@ -54,32 +59,33 @@ namespace Barotrauma } else { - ActiveOrders.Add(new Pair(order, fadeOutTime)); + ActiveOrders.Add(new Pair(order, fadeOutTime)); return true; } } - public void RemoveOrder(Order order) - { - ActiveOrders.RemoveAll(o => o.First == order); - } - public void AddCharacterElements(XElement element) { - foreach (XElement subElement in element.Elements()) + foreach (XElement characterElement in element.Elements()) { - if (!subElement.Name.ToString().Equals("character", StringComparison.OrdinalIgnoreCase)) { continue; } + if (!characterElement.Name.ToString().Equals("character", StringComparison.OrdinalIgnoreCase)) { continue; } - CharacterInfo characterInfo = new CharacterInfo(subElement); + CharacterInfo characterInfo = new CharacterInfo(characterElement); #if CLIENT - if (subElement.GetAttributeBool("lastcontrolled", false)) { characterInfo.LastControlled = true; } + if (characterElement.GetAttributeBool("lastcontrolled", false)) { characterInfo.LastControlled = true; } #endif characterInfos.Add(characterInfo); - foreach (XElement invElement in subElement.Elements()) + foreach (XElement subElement in characterElement.Elements()) { - if (!invElement.Name.ToString().Equals("inventory", StringComparison.OrdinalIgnoreCase)) { continue; } - characterInfo.InventoryData = invElement; - break; + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "inventory": + characterInfo.InventoryData = subElement; + break; + case "health": + characterInfo.HealthData = subElement; + break; + } } } } @@ -118,6 +124,12 @@ namespace Barotrauma AddCharacterToCrewList(character); AddCurrentOrderIcon(character, character.CurrentOrder, character.CurrentOrderOption); #endif + var idleObjective = character.AIController?.ObjectiveManager?.GetObjective(); + if (idleObjective != null) + { + idleObjective.Behavior = character.Info.Job.Prefab.IdleBehavior; + } + } public void AddCharacterInfo(CharacterInfo characterInfo) @@ -175,7 +187,7 @@ namespace Barotrauma } if (character.Info.InventoryData != null) { - character.Info.SpawnInventoryItems(character.Inventory, character.Info.InventoryData); + character.SpawnInventoryItems(character.Inventory, character.Info.InventoryData); } else if (!character.Info.StartItemsGiven) { @@ -206,14 +218,19 @@ namespace Barotrauma public void Update(float deltaTime) { - foreach (Pair order in ActiveOrders) + foreach (Pair order in ActiveOrders) { - order.Second -= deltaTime; + if (order.Second.HasValue) { order.Second -= deltaTime; } } - ActiveOrders.RemoveAll(o => o.Second <= 0.0f); + ActiveOrders.RemoveAll(o => o.Second.HasValue && o.Second <= 0.0f); UpdateConversations(deltaTime); UpdateProjectSpecific(deltaTime); + ActiveReadyCheck?.Update(deltaTime); + if (ActiveReadyCheck != null && ActiveReadyCheck.IsFinished) + { + ActiveReadyCheck = null; + } } #region Dialog diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index 11cb7f46f..f4070f5df 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -146,7 +146,7 @@ namespace Barotrauma { for (int i = 0; i < wall.SectionCount; i++) { - wall.AddDamage(i, -wall.MaxHealth); + wall.SetDamage(i, 0, createNetworkEvent: false); } } } @@ -181,6 +181,11 @@ namespace Barotrauma } } + /// + /// Automatically cleared after triggering -> no need to unregister + /// + public event Action BeforeLevelLoading; + public void LoadNewLevel() { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) @@ -194,6 +199,9 @@ namespace Barotrauma return; } + BeforeLevelLoading?.Invoke(); + BeforeLevelLoading = null; + if (Level.Loaded == null || Submarine.MainSub == null) { LoadInitialLevel(); @@ -504,6 +512,12 @@ namespace Barotrauma Map.SetLocation(Map.Locations.IndexOf(Map.StartLocation)); Map.SelectLocation(-1); EndCampaignProjSpecific(); + + if (CampaignMetadata != null) + { + int loops = CampaignMetadata.GetInt("campaign.endings", 0); + CampaignMetadata.SetValue("campaign.endings", loops + 1); + } } protected virtual void EndCampaignProjSpecific() { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CoOpMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CoOpMode.cs new file mode 100644 index 000000000..8df39d58e --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CoOpMode.cs @@ -0,0 +1,11 @@ +using System; + +namespace Barotrauma +{ + class CoOpMode : MissionMode + { + public CoOpMode(GameModePreset preset, MissionPrefab missionPrefab) : base(preset, ValidateMissionPrefab(missionPrefab, 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/GameModePreset.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameModePreset.cs index 93371aa85..608dd2b89 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameModePreset.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameModePreset.cs @@ -12,6 +12,7 @@ namespace Barotrauma public static GameModePreset MultiPlayerCampaign; public static GameModePreset Tutorial; public static GameModePreset Mission; + public static GameModePreset PvP; public static GameModePreset TestMode; public static GameModePreset Sandbox; public static GameModePreset DevSandbox; @@ -51,7 +52,8 @@ namespace Barotrauma TestMode = new GameModePreset("testmode", typeof(TestGameMode), true); #endif Sandbox = new GameModePreset("sandbox", typeof(GameMode), false); - Mission = new GameModePreset("mission", typeof(MissionMode), false); + Mission = new GameModePreset("mission", typeof(CoOpMode), false); + PvP = new GameModePreset("pvp", typeof(PvPMode), false); MultiPlayerCampaign = new GameModePreset("multiplayercampaign", typeof(MultiPlayerCampaign), false, false); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MissionMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MissionMode.cs index c5f8ca179..39193b8a2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MissionMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MissionMode.cs @@ -1,6 +1,9 @@ -namespace Barotrauma +using System; +using System.Collections.Generic; + +namespace Barotrauma { - partial class MissionMode : GameMode + abstract partial class MissionMode : GameMode { private readonly Mission mission; @@ -25,5 +28,29 @@ Location[] locations = { GameMain.GameSession.StartLocation, GameMain.GameSession.EndLocation }; mission = Mission.LoadRandom(locations, seed, false, missionType); } + + protected static MissionPrefab ValidateMissionPrefab(MissionPrefab missionPrefab, Dictionary missionClasses) + { + if (ValidateMissionType(missionPrefab.Type, missionClasses) != missionPrefab.Type) + { + throw new InvalidOperationException("Cannot start gamemode with mission type " + missionPrefab.Type); + } + return missionPrefab; + } + + protected static MissionType ValidateMissionType(MissionType missionType, Dictionary missionClasses) + { + var missionTypes = (MissionType[])Enum.GetValues(typeof(MissionType)); + for (int i = 0; i < missionTypes.Length; i++) + { + var type = missionTypes[i]; + if (type == MissionType.None || type == MissionType.All) { continue; } + if (!missionClasses.ContainsKey(type)) + { + missionType &= ~(type); + } + } + return missionType; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/PvPMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/PvPMode.cs new file mode 100644 index 000000000..4b3ecdc8d --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/PvPMode.cs @@ -0,0 +1,11 @@ +using System; + +namespace Barotrauma +{ + class PvPMode : MissionMode + { + public PvPMode(GameModePreset preset, MissionPrefab missionPrefab) : base(preset, ValidateMissionPrefab(missionPrefab, MissionPrefab.PvPMissionClasses)) { } + + public PvPMode(GameModePreset preset, MissionType missionType, string seed) : base(preset, ValidateMissionType(missionType, MissionPrefab.PvPMissionClasses), seed) { } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index 61b0d7c6f..42d147f3d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -160,11 +160,17 @@ namespace Barotrauma private GameMode InstantiateGameMode(GameModePreset gameModePreset, string seed, MissionPrefab missionPrefab = null, MissionType missionType = MissionType.None) { - if (gameModePreset.GameModeType == typeof(MissionMode)) + if (gameModePreset.GameModeType == typeof(CoOpMode)) { return missionPrefab != null ? - new MissionMode(gameModePreset, missionPrefab) : - new MissionMode(gameModePreset, missionType, seed ?? ToolBox.RandomSeed(8)); + new CoOpMode(gameModePreset, missionPrefab) : + new CoOpMode(gameModePreset, missionType, seed ?? ToolBox.RandomSeed(8)); + } + else if (gameModePreset.GameModeType == typeof(PvPMode)) + { + return missionPrefab != null ? + new PvPMode(gameModePreset, missionPrefab) : + new PvPMode(gameModePreset, missionType, seed ?? ToolBox.RandomSeed(8)); } else if (gameModePreset.GameModeType == typeof(MultiPlayerCampaign)) { @@ -382,7 +388,7 @@ namespace Barotrauma } Entity.Spawner = new EntitySpawner(); - + if (GameMode.Mission != null) { Mission = GameMode.Mission; } if (GameMode != null) { GameMode.Start(); } if (GameMode.Mission != null) @@ -411,6 +417,7 @@ namespace Barotrauma //the server does this after loading the respawn shuttle Level?.SpawnNPCs(); Level?.SpawnCorpses(); + Level?.PrepareBeaconStation(); AutoItemPlacer.PlaceIfNeeded(); } if (GameMode is MultiPlayerCampaign mpCampaign) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/ReadyCheck.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/ReadyCheck.cs new file mode 100644 index 000000000..a1b6bed04 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/ReadyCheck.cs @@ -0,0 +1,54 @@ +#nullable enable +using System; +using System.Collections.Generic; + +namespace Barotrauma +{ + internal enum ReadyStatus + { + Unanswered, + Yes, + No, + } + + internal partial class ReadyCheck + { + private readonly float endTime; + private float time; + public readonly Dictionary Clients; + public bool IsFinished = false; + + public ReadyCheck(List clients, float duration = 30) + { + Clients = new Dictionary(); + foreach (byte client in clients) + { + if (Clients.ContainsKey(client)) { continue; } + + Clients.Add(client, ReadyStatus.Unanswered); + } + + time = duration; + endTime = duration; +#if CLIENT + lastSecond = (int) Math.Ceiling(duration); +#endif + } + + partial void EndReadyCheck(); + + public void Update(float deltaTime) + { + if (time > 0) + { +#if CLIENT + UpdateBar(); +#endif + time -= deltaTime; + return; + } + + EndReadyCheck(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs index 1f653b3c1..b819a3763 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs @@ -585,7 +585,8 @@ namespace Barotrauma if (files.Any(f => f.Type == ContentType.Submarine || f.Type == ContentType.Outpost || f.Type == ContentType.OutpostModule || - f.Type == ContentType.Wreck)) { SubmarineInfo.RefreshSavedSubs(); } + f.Type == ContentType.Wreck || + f.Type == ContentType.BeaconStation)) { SubmarineInfo.RefreshSavedSubs(); } if (files.Any(f => f.Type == ContentType.NPCSets)) { NPCSet.LoadSets(); } if (files.Any(f => f.Type == ContentType.OutpostConfig)) { OutpostGenerationParams.LoadPresets(); } if (files.Any(f => f.Type == ContentType.Factions)) { FactionPrefab.LoadFactions(); } @@ -602,6 +603,7 @@ namespace Barotrauma if (files.Any(f => f.Type == ContentType.LevelObjectPrefabs)) { LevelObjectPrefab.LoadAll(); } if (files.Any(f => f.Type == ContentType.MapGenerationParameters)) { MapGenerationParams.Init(); } if (files.Any(f => f.Type == ContentType.LevelGenerationParameters)) { LevelGenerationParams.LoadPresets(); } + if (files.Any(f => f.Type == ContentType.CaveGenerationParameters)) { CaveGenerationParams.LoadPresets(); } if (files.Any(f => f.Type == ContentType.TraitorMissions)) { TraitorMissionPrefab.Init(); } if (files.Any(f => f.Type == ContentType.Orders)) { Order.Init(); } if (files.Any(f => f.Type == ContentType.EventManagerSettings)) { EventManagerSettings.Init(); } @@ -635,6 +637,7 @@ namespace Barotrauma ContentType.LocationTypes, ContentType.MapGenerationParameters, ContentType.LevelGenerationParameters, + ContentType.CaveGenerationParameters, ContentType.Sounds, ContentType.Particles, ContentType.Decals, @@ -645,6 +648,7 @@ namespace Barotrauma ContentType.Factions, ContentType.Wreck, ContentType.WreckAIConfig, + ContentType.BeaconStation, ContentType.BackgroundCreaturePrefabs, ContentType.ServerExecutable, ContentType.TraitorMissions, @@ -834,7 +838,7 @@ namespace Barotrauma } } - private void LoadDefaultConfig(bool setLanguage = true) + private void LoadDefaultConfig(bool setLanguage = true, bool loadContentPackages = true) { XDocument doc = XMLExtensions.TryLoadXml(SavePath); if (doc == null) @@ -866,7 +870,10 @@ namespace Barotrauma #if CLIENT LoadControls(doc); #endif - LoadContentPackages(doc); + if (loadContentPackages) + { + LoadContentPackages(doc); + } #if DEBUG WindowMode = WindowMode.Windowed; diff --git a/Barotrauma/BarotraumaShared/SharedSource/InputType.cs b/Barotrauma/BarotraumaShared/SharedSource/InputType.cs index 09fab09a7..3c26158fa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/InputType.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/InputType.cs @@ -17,11 +17,8 @@ namespace Barotrauma Deselect, Shoot, Command, - ToggleInventory -#if DEBUG - , + ToggleInventory, NextFireMode, PreviousFireMode -#endif } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs index ee9984e03..34da543e7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs @@ -89,6 +89,16 @@ namespace Barotrauma.Items.Components } } + /// + /// Automatically cleared after docking -> no need to unregister + /// + public event Action OnDocked; + + /// + /// Automatically cleared after undocking -> no need to unregister + /// + public event Action OnUnDocked; + public DockingPort(Item item, XElement element) : base(item, element) { @@ -213,6 +223,9 @@ namespace Barotrauma.Items.Components item.CreateServerEvent(this); } #endif + + OnDocked?.Invoke(); + OnDocked = null; } @@ -817,6 +830,9 @@ namespace Barotrauma.Items.Components docked = false; + Item.Submarine.EnableObstructedWaypoints(DockingTarget.Item.Submarine); + obstructedWayPointsDisabled = false; + DockingTarget.Undock(); DockingTarget = null; @@ -860,9 +876,6 @@ namespace Barotrauma.Items.Components outsideBlocker?.Body.Remove(outsideBlocker); outsideBlocker = null; - Item.Submarine.EnableObstructedWaypoints(); - obstructedWayPointsDisabled = false; - #if SERVER if (GameMain.Server != null && (!item.Submarine?.Loading ?? true)) { @@ -870,6 +883,8 @@ namespace Barotrauma.Items.Components item.CreateServerEvent(this); } #endif + OnUnDocked?.Invoke(); + OnUnDocked = null; } public override void Update(float deltaTime, Camera cam) @@ -1034,7 +1049,6 @@ namespace Barotrauma.Items.Components Dock(dockingPort); } } - } public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs index 11cc23a71..e62daeaa4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs @@ -23,6 +23,20 @@ namespace Barotrauma.Items.Components private readonly Sprite doorSprite, weldedSprite, brokenSprite; private readonly bool scaleBrokenSprite, fadeBrokenSprite; private readonly bool autoOrientGap; + + private bool isJammed; + public bool IsJammed + { + get { return isJammed; } + set + { + if (isJammed == value) { return; } + isJammed = value; +#if SERVER + item.CreateServerEvent(this); +#endif + } + } private bool isStuck; public bool IsStuck @@ -297,7 +311,7 @@ namespace Barotrauma.Items.Components { if (toggleCooldownTimer > 0.0f && user != lastUser) { OnFailedToOpen(); return; } toggleCooldownTimer = ToggleCoolDown; - if (IsStuck) { toggleCooldownTimer = 1.0f; OnFailedToOpen(); return; } + if (IsStuck || IsJammed) { toggleCooldownTimer = 1.0f; OnFailedToOpen(); return; } lastUser = user; SetState(PredictedState == null ? !isOpen : !PredictedState.Value, false, true, forcedOpen: actionType == ActionType.OnPicked); } @@ -341,7 +355,7 @@ namespace Barotrauma.Items.Components } bool isClosing = false; - if (!IsStuck) + if ((!IsStuck && !IsJammed) || !isOpen) { if (PredictedState == null) { @@ -630,7 +644,7 @@ namespace Barotrauma.Items.Components public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f) { - if (IsStuck) { return; } + if (IsStuck || IsJammed) { return; } bool wasOpen = PredictedState == null ? isOpen : PredictedState.Value; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs index 582282b04..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; @@ -193,7 +193,7 @@ namespace Barotrauma.Items.Components partial void DischargeProjSpecific(); - private void FindNodes(Vector2 worldPosition, float range) + public void FindNodes(Vector2 worldPosition, float range) { //see which submarines are within range so we can skip structures that are in far-away subs List submarinesInRange = new List(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs index ea5ade1d9..3e98a264c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Numerics; using System.Xml.Linq; using Barotrauma.Extensions; using Barotrauma.Networking; @@ -10,6 +9,7 @@ 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 { @@ -138,19 +138,25 @@ namespace Barotrauma.Items.Components public TileSide Sides = TileSide.None; public TileSide BlockedSides = TileSide.None; - public readonly FoliageConfig FlowerConfig; - public readonly FoliageConfig LeafConfig; + public FoliageConfig FlowerConfig; + public FoliageConfig LeafConfig; public int FailedGrowthAttempts; public Rectangle Rect; public Vector2 Position; - public Color HealthColor = Color.Transparent; - public float DecayDelay; - private float VineStep; - private float FlowerStep; + private readonly float diameter; + public Vector2 offset; + + public VineTileType Type; + public readonly Dictionary AdjacentPositions; + public static int Size = 32; + + + public float VineStep; + public float FlowerStep; + private float growthStep; - public float GrowthStep { get => growthStep; @@ -166,17 +172,12 @@ namespace Barotrauma.Items.Components } } - private readonly float diameter; - private Vector2 offset; + public Color HealthColor = Color.Transparent; + public float DecayDelay; - private readonly Growable Parent; - public VineTileType Type; + private readonly Growable? Parent; - public readonly Dictionary AdjacentPositions; - - public static int Size = 32; - - public VineTile(Growable parent, Vector2 position, VineTileType type, FoliageConfig? flowerConfig = null, FoliageConfig? leafConfig = null, Rectangle? rect = null) + public VineTile(Growable? parent, Vector2 position, VineTileType type, FoliageConfig? flowerConfig = null, FoliageConfig? leafConfig = null, Rectangle? rect = null) { FlowerConfig = flowerConfig ?? FoliageConfig.EmptyConfig; LeafConfig = leafConfig ?? FoliageConfig.EmptyConfig; @@ -197,7 +198,9 @@ namespace Barotrauma.Items.Components public void UpdateScale(float deltaTime) { - if (Parent.Decayed && GrowthStep > 1.0f) + bool decayed = Parent?.Decayed ?? false; + + if (decayed && GrowthStep > 1.0f) { if (DecayDelay > 0) { @@ -209,7 +212,7 @@ namespace Barotrauma.Items.Components } } - if (GrowthStep >= 2.0f || Parent.Decayed) { return; } + if (GrowthStep >= 2.0f || decayed) { return; } GrowthStep += deltaTime; @@ -282,13 +285,26 @@ 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); } public bool CanGrowMore() => (Sides | BlockedSides).Count() < 4; + public bool IsSideBlocked(TileSide side) => BlockedSides.IsBitSet(side) || Sides.IsBitSet(side); + public static Rectangle CreatePlantRect(Vector2 pos) => new Rectangle((int) pos.X - Size / 2, (int) pos.Y + Size / 2, Size, Size); } @@ -313,6 +329,11 @@ namespace Barotrauma.Items.Components return count; } + + public static TileSide GetOppositeSide(this TileSide side) + { + return (TileSide) (1 << ((int) Math.Log2((int) side) + 2) % 4); + } } internal partial class Growable : ItemComponent, IServerSerializable @@ -371,6 +392,9 @@ 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; } + private const float increasedDeathSpeed = 10f; private bool accelerateDeath; private float health; @@ -666,7 +690,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); @@ -705,8 +745,7 @@ namespace Barotrauma.Items.Components // if the X value is bigger than Y it's to the left or right of us and then check if X is negative or positive to determine if it's right or left TileSide connectingSide = absDistX > absDistY ? distX > 0 ? TileSide.Right : TileSide.Left : distY > 0 ? TileSide.Top : TileSide.Bottom; - // We use log2 to find the index and offset that index by 2 since the opposite side is always 2 offsets away - TileSide oppositeSide = (TileSide) (1 << ((int) Math.Log2((int) connectingSide) + 2) % 4); + TileSide oppositeSide = connectingSide.GetOppositeSide(); if (otherVine.BlockedSides.IsBitSet(connectingSide)) { @@ -810,9 +849,9 @@ namespace Barotrauma.Items.Components return element; } - public override void Load(XElement componentElement, bool usePrefabValues) + public override void Load(XElement componentElement, bool usePrefabValues, IdRemap idRemap) { - base.Load(componentElement, usePrefabValues); + base.Load(componentElement, usePrefabValues, idRemap); flowerTiles = componentElement.GetAttributeIntArray("flowertiles", new int[0]); Decayed = componentElement.GetAttributeBool("decayed", false); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index c6c2f54af..4bd9ad9ce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -30,6 +30,7 @@ namespace Barotrauma.Items.Components private float swingState; private bool attachable, attached, attachedByDefault; + private Voronoi2.VoronoiCell attachTargetCell; private readonly PhysicsBody body; public PhysicsBody Pusher { @@ -213,9 +214,9 @@ namespace Barotrauma.Items.Components } } - public override void Load(XElement componentElement, bool usePrefabValues) + public override void Load(XElement componentElement, bool usePrefabValues, IdRemap idRemap) { - base.Load(componentElement, usePrefabValues); + base.Load(componentElement, usePrefabValues, idRemap); if (usePrefabValues) { @@ -255,6 +256,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) { @@ -359,7 +361,7 @@ namespace Barotrauma.Items.Components public override void Unequip(Character character) { - if (picker == null) return; + if (picker == null) { return; } picker.DeselectItem(item); #if SERVER @@ -383,9 +385,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; } @@ -406,7 +408,7 @@ namespace Barotrauma.Items.Components if (item.CurrentHull == null) { - return Structure.GetAttachTarget(item.WorldPosition) != null; + return attachTargetCell != null && Structure.GetAttachTarget(item.WorldPosition) != null; } else { @@ -464,7 +466,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,6 +481,11 @@ namespace Barotrauma.Items.Components } item.Submarine = attachTarget.Submarine; } + else + { + attachTargetCell = GetAttachTargetCell(150.0f); + if (attachTargetCell != null) { IsActive = true; } + } } var containedItems = item.OwnInventory?.Items; @@ -507,6 +514,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 +576,47 @@ 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) + { + 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) + { + 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 +626,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) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs new file mode 100644 index 000000000..fd67498e6 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs @@ -0,0 +1,54 @@ +using Microsoft.Xna.Framework; +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Barotrauma.Items.Components +{ + partial class IdCard : Pickable + { + public IdCard(Item item, XElement element) : base(item, element) + { + + } + + public void Initialize(CharacterInfo info) + { + if (info == null) return; + + if (info.Job?.Prefab != null) + { + item.AddTag("jobid:" + info.Job.Prefab.Identifier); + } + + var head = info.Head; + + if (info != null && head != null) + { + item.AddTag("gender:" + head.gender.ToString().ToLowerInvariant()); + item.AddTag("race:" + head.race.ToString()); + item.AddTag("headspriteid:" + info.HeadSpriteId.ToString()); + item.AddTag("hairindex:" + head.HairIndex); + item.AddTag("beardindex:" + head.BeardIndex); + item.AddTag("moustacheindex:" + head.MoustacheIndex); + item.AddTag("faceattachmentindex:" + head.FaceAttachmentIndex); + + if (head.SheetIndex != null) + { + item.AddTag("sheetindex:" + head.SheetIndex.Value.X + ";" + head.SheetIndex.Value.Y); + } + } + } + + public override void Equip(Character character) + { + base.Equip(character); + character.Info.CheckDisguiseStatus(true, this); + } + + public override void Unequip(Character character) + { + base.Unequip(character); + 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 60823a5d5..adf3e7c01 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs @@ -33,6 +33,9 @@ namespace Barotrauma.Items.Components { return; } + + if (holdable == null) { return; } + deattachTimer = Math.Max(0.0f, value); #if SERVER if (deattachTimer >= DeattachDuration) @@ -57,7 +60,7 @@ namespace Barotrauma.Items.Components public bool Attached { - get { return holdable == null ? false : holdable.Attached; } + get { return holdable != null && holdable.Attached; } } public LevelResource(Item item, XElement element) : base(item, element) @@ -67,14 +70,14 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { - if (!holdable.Attached) + if (holdable != null && !holdable.Attached) { trigger.Enabled = false; IsActive = false; } 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); } @@ -87,7 +90,6 @@ namespace Barotrauma.Items.Components holdable = item.GetComponent(); if (holdable == null) { - DebugConsole.ThrowError("Error while initializing item \"" + item.Name + "\". Level resources require a Holdable component."); IsActive = false; return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index 6c4d2e8e0..dad1c77cf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -143,13 +143,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.HasSelectedItem(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; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs index 107873037..967b4ecb7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs @@ -5,6 +5,7 @@ using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Linq; using System.Xml.Linq; namespace Barotrauma.Items.Components @@ -128,54 +129,19 @@ namespace Barotrauma.Items.Components for (int i = 0; i < ProjectileCount; i++) { Projectile projectile = FindProjectile(triggerOnUseOnContainers: true); - if (projectile == null) { return true; } - - float spread = GetSpread(character); - float rotation = (item.body.Dir == 1.0f) ? item.body.Rotation : item.body.Rotation - MathHelper.Pi; - rotation += spread * Rand.Range(-0.5f, 0.5f); - - projectile.User = character; - //add the limbs of the shooter to the list of bodies to be ignored - //so that the player can't shoot himself - projectile.IgnoredBodies = new List(limbBodies); - - Vector2 projectilePos = item.SimPosition; - Vector2 sourcePos = character?.AnimController == null ? item.SimPosition : character.AnimController.AimSourceSimPos; - Vector2 barrelPos = TransformedBarrelPos + item.body.SimPosition; - //make sure there's no obstacles between the base of the weapon (or the shoulder of the character) and the end of the barrel - if (Submarine.PickBody(sourcePos, barrelPos, projectile.IgnoredBodies, Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking) == null) + if (projectile != null) { - //no obstacles -> we can spawn the projectile at the barrel - projectilePos = barrelPos; - } - else if ((sourcePos - barrelPos).LengthSquared() > 0.0001f) - { - //spawn the projectile body.GetMaxExtent() away from the position where the raycast hit the obstacle - projectilePos = sourcePos - Vector2.Normalize(barrelPos - projectilePos) * Math.Max(projectile.Item.body.GetMaxExtent(), 0.1f); - } - - projectile.Item.body.ResetDynamics(); - projectile.Item.SetTransform(projectilePos, rotation); - - projectile.Use(deltaTime); - projectile.Item.GetComponent()?.Attach(item, projectile.Item); - if (projectile.Item.Removed) { continue; } - projectile.User = character; - - projectile.Item.body.ApplyTorque(projectile.Item.body.Mass * degreeOfFailure * Rand.Range(-10.0f, 10.0f)); - - //set the rotation of the projectile again because dropping the projectile resets the rotation - projectile.Item.SetTransform(projectilePos, - rotation + (projectile.Item.body.Dir * projectile.LaunchRotationRadians)); - - item.RemoveContained(projectile.Item); - - if (i == 0) - { - //recoil - item.body.ApplyLinearImpulse( - new Vector2((float)Math.Cos(projectile.Item.body.Rotation), (float)Math.Sin(projectile.Item.body.Rotation)) * item.body.Mass * -50.0f, - maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + Vector2 barrelPos = TransformedBarrelPos + item.body.SimPosition; + float rotation = (Item.body.Dir == 1.0f) ? Item.body.Rotation : Item.body.Rotation - MathHelper.Pi; + float spread = GetSpread(character) * Rand.Range(-0.5f, 0.5f); + projectile.Shoot(character, character.AnimController.AimSourceSimPos, barrelPos, rotation + spread, ignoredBodies: limbBodies.ToList(), createNetworkEvent: false); + projectile.Item.GetComponent()?.Attach(Item, projectile.Item); + if (i == 0) + { + Item.body.ApplyLinearImpulse(new Vector2((float)Math.Cos(projectile.Item.body.Rotation), (float)Math.Sin(projectile.Item.body.Rotation)) * Item.body.Mass * -50.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + } + projectile.Item.body.ApplyTorque(projectile.Item.body.Mass * degreeOfFailure * Rand.Range(-10.0f, 10.0f)); + Item.RemoveContained(projectile.Item); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index f218cfcee..b0e4daf2f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Xml.Linq; using Barotrauma.Extensions; +using Barotrauma.MapCreatures.Behavior; namespace Barotrauma.Items.Components { @@ -53,6 +54,18 @@ namespace Barotrauma.Items.Components get; set; } + [Serialize(0.0f, false, description: "How much damage is applied to ballast flora.")] + public float FireDamage + { + get; set; + } + + [Serialize(0.0f, false, description: "How many units of damage the item removes from destructible level walls per second.")] + public float LevelWallFixAmount + { + get; set; + } + [Serialize(0.0f, false, description: "How much the item decreases the size of fires per second.")] public float ExtinguishAmount { @@ -183,23 +196,40 @@ namespace Barotrauma.Items.Components } Vector2 rayStart; + Vector2 rayStartWorld; Vector2 sourcePos = character?.AnimController == null ? item.SimPosition : character.AnimController.AimSourceSimPos; Vector2 barrelPos = item.SimPosition + ConvertUnits.ToSimUnits(TransformedBarrelPos); //make sure there's no obstacles between the base of the item (or the shoulder of the character) and the end of the barrel if (Submarine.PickBody(sourcePos, barrelPos, collisionCategory: Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking) == null) { //no obstacles -> we start the raycast at the end of the barrel - rayStart = ConvertUnits.ToSimUnits(item.WorldPosition + TransformedBarrelPos); + rayStart = ConvertUnits.ToSimUnits(item.Position + TransformedBarrelPos); + rayStartWorld = ConvertUnits.ToSimUnits(item.WorldPosition + TransformedBarrelPos); } else { - rayStart = Submarine.LastPickedPosition + Submarine.LastPickedNormal * 0.1f; - if (item.Submarine != null) { rayStart += item.Submarine.SimPosition; } + rayStart = rayStartWorld = Submarine.LastPickedPosition + Submarine.LastPickedNormal * 0.1f; + if (item.Submarine != null) { rayStartWorld += item.Submarine.SimPosition; } + } + + //if the calculated barrel pos is in another hull, use the origin of the item to make sure the particles don't end up in an incorrect hull + if (item.CurrentHull != null) + { + var barrelHull = Hull.FindHull(ConvertUnits.ToDisplayUnits(rayStartWorld), item.CurrentHull, useWorldCoordinates: true); + if (barrelHull != null && barrelHull != item.CurrentHull) + { + if (MathUtils.GetLineRectangleIntersection(ConvertUnits.ToDisplayUnits(sourcePos), ConvertUnits.ToDisplayUnits(rayStart), item.CurrentHull.Rect, out Vector2 hullIntersection)) + { + Vector2 rayDir = rayStart.NearlyEquals(sourcePos) ? Vector2.Zero : Vector2.Normalize(rayStart - sourcePos); + rayStartWorld = ConvertUnits.ToSimUnits(hullIntersection - rayDir * 5.0f); + if (item.Submarine != null) { rayStartWorld += item.Submarine.SimPosition; } + } + } } 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 = rayStart + + Vector2 rayEnd = rayStartWorld + ConvertUnits.ToSimUnits(new Vector2( (float)Math.Cos(angle), (float)Math.Sin(angle)) * Range * item.body.Dir); @@ -218,7 +248,7 @@ namespace Barotrauma.Items.Components IsActive = true; activeTimer = 0.1f; - debugRayStartPos = ConvertUnits.ToDisplayUnits(rayStart); + debugRayStartPos = ConvertUnits.ToDisplayUnits(rayStartWorld); debugRayEndPos = ConvertUnits.ToDisplayUnits(rayEnd); Submarine parentSub = character?.Submarine ?? item.Submarine; @@ -232,16 +262,16 @@ namespace Barotrauma.Items.Components { continue; } - Repair(rayStart - sub.SimPosition, rayEnd - sub.SimPosition, deltaTime, character, degreeOfSuccess, ignoredBodies); + Repair(rayStartWorld - sub.SimPosition, rayEnd - sub.SimPosition, deltaTime, character, degreeOfSuccess, ignoredBodies); } - Repair(rayStart, rayEnd, deltaTime, character, degreeOfSuccess, ignoredBodies); + Repair(rayStartWorld, rayEnd, deltaTime, character, degreeOfSuccess, ignoredBodies); } else { - Repair(rayStart - parentSub.SimPosition, rayEnd - parentSub.SimPosition, deltaTime, character, degreeOfSuccess, ignoredBodies); + Repair(rayStartWorld - parentSub.SimPosition, rayEnd - parentSub.SimPosition, deltaTime, character, degreeOfSuccess, ignoredBodies); } - UseProjSpecific(deltaTime, rayStart); + UseProjSpecific(deltaTime, rayStartWorld); return true; } @@ -289,6 +319,7 @@ namespace Barotrauma.Items.Components { if (RepairThroughHoles && f.IsSensor && f.Body?.UserData is Structure || (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; }, allowInsideFixture: true); @@ -324,9 +355,16 @@ namespace Barotrauma.Items.Components hitCharacters.Add(hitCharacter); } + //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) + float thisBodyFraction = Submarine.LastPickedBodyDist(body); + if (!RepairThroughWalls && lastHitType == typeof(Structure) && Range * (thisBodyFraction - lastPickedFraction) > 100.0f) + { + break; + } if (FixBody(user, deltaTime, degreeOfSuccess, body)) { - lastPickedFraction = Submarine.LastPickedBodyDist(body); + lastPickedFraction = thisBodyFraction; if (bodyType != null) { lastHitType = bodyType; } } } @@ -341,6 +379,8 @@ namespace Barotrauma.Items.Components { if (RepairThroughHoles && f.IsSensor && f.Body?.UserData is Structure) { return false; } if (f.Body?.UserData as string == "ruinroom") { return false; } + if (f.Body?.UserData is VineTile && !(FireDamage > 0)) { return false; } + if (f.Body?.UserData is Item targetItem) { if (!HitItems) { return false; } @@ -479,6 +519,15 @@ namespace Barotrauma.Items.Components } return true; } + 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) + { + levelWall.AddDamage(-LevelWallFixAmount * deltaTime, item.WorldPosition); + } + return true; + } else if (targetBody.UserData is Character targetCharacter) { if (targetCharacter.Removed) { return false; } @@ -569,6 +618,13 @@ namespace Barotrauma.Items.Components FixItemProjSpecific(user, deltaTime, targetItem); return true; } + else if (targetBody.UserData is BallastFloraBranch branch) + { + if (branch.ParentBallastFlora is { } ballastFlora) + { + ballastFlora.DamageBranch(branch, FireDamage * deltaTime, BallastFloraBehavior.AttackType.Fire, user); + } + } return false; } @@ -769,7 +825,8 @@ namespace Barotrauma.Items.Components object value = property.GetValue(target); if (door.Stuck > 0) { - var progressBar = user.UpdateHUDProgressBar(door, door.Item.WorldPosition, door.Stuck / 100, Color.DarkGray * 0.5f, Color.White, "progressbar.welding"); + var progressBar = user.UpdateHUDProgressBar(door, door.Item.WorldPosition, door.Stuck / 100, Color.DarkGray * 0.5f, Color.White, + effect.propertyEffects[i].GetType() == typeof(float) && (float)effect.propertyEffects[i] < 0 ? "progressbar.cutting" : "progressbar.welding"); if (progressBar != null) { progressBar.Size = new Vector2(60.0f, 20.0f); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index cddb7d1f2..deb68328f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -394,7 +394,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) @@ -763,7 +766,7 @@ namespace Barotrauma.Items.Components } } - public virtual void Load(XElement componentElement, bool usePrefabValues) + public virtual void Load(XElement componentElement, bool usePrefabValues, IdRemap idRemap) { if (componentElement != null) { @@ -963,12 +966,12 @@ namespace Barotrauma.Items.Components 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,11 +989,21 @@ 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; } @@ -1011,6 +1024,7 @@ namespace Barotrauma.Items.Components if (FindSuitableContainer(character, i => { + if (i.IsThisOrAnyContainerIgnoredByAI()) { return 0; } var container = i.GetComponent(); if (container == null) { return 0; } if (container.Inventory.IsFull()) { return 0; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index 3ffb755a1..133216387 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -94,6 +94,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(); @@ -377,12 +380,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.Items.Any(it => it == null)) { - if (Inventory != null && Inventory.Items.Any(it => it == null)) - { - Entity.Spawner?.AddToSpawnQueue(prefab, Inventory); - } + Entity.Spawner?.AddToSpawnQueue(prefab, Inventory, spawnIfInventoryFull: false); } } } @@ -415,17 +415,17 @@ namespace Barotrauma.Items.Components } } - public override void Load(XElement componentElement, bool usePrefabValues) + public override void Load(XElement componentElement, bool usePrefabValues, IdRemap idRemap) { - base.Load(componentElement, usePrefabValues); + base.Load(componentElement, usePrefabValues, idRemap); string containedString = componentElement.GetAttributeString("contained", ""); string[] itemIdStrings = containedString.Split(','); itemIds = new ushort[itemIdStrings.Length]; for (int i = 0; i < itemIdStrings.Length; i++) { - if (!ushort.TryParse(itemIdStrings[i], out ushort id)) { continue; } - itemIds[i] = id; + if (!int.TryParse(itemIdStrings[i], out int id)) { continue; } + itemIds[i] = idRemap.GetOffsetId(id); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs index 0df74b078..d32d542bb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs @@ -318,16 +318,12 @@ namespace Barotrauma.Items.Components if (!character.IsRemotePlayer || character.ViewTarget == focusTarget) { - Vector2 centerPos = new Vector2(item.WorldRect.Center.X, item.WorldRect.Center.Y); + Vector2 centerPos = new Vector2(focusTarget.WorldRect.Center.X, focusTarget.WorldRect.Center.Y); - Item targetItem = focusTarget as Item; - if (targetItem != null) + Turret turret = focusTarget.GetComponent(); + if (turret != null) { - Turret turret = targetItem.GetComponent(); - if (turret != null) - { - centerPos = new Vector2(targetItem.WorldRect.X + turret.TransformedBarrelPos.X, targetItem.WorldRect.Y - turret.TransformedBarrelPos.Y); - } + centerPos = new Vector2(focusTarget.WorldRect.X + turret.TransformedBarrelPos.X, focusTarget.WorldRect.Y - turret.TransformedBarrelPos.Y); } Vector2 offset = character.CursorWorldPosition - centerPos; @@ -356,6 +352,9 @@ namespace Barotrauma.Items.Components public override bool Pick(Character picker) { +#if CLIENT + if (Screen.Selected == GameMain.SubEditorScreen) { return false; } +#endif if (IsToggle) { if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs index 05dec7c0d..660f1417a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs @@ -123,7 +123,7 @@ namespace Barotrauma.Items.Components //drop all items that are inside the deconstructed item foreach (ItemContainer ic in targetItem.GetComponents()) { - if (ic?.Inventory?.Items == null) { continue; } + if (ic?.Inventory?.Items == null || ic.RemoveContainedItemsOnDeconstruct) { continue; } foreach (Item containedItem in ic.Inventory.Items) { containedItem?.Drop(dropper: null, createNetworkEvent: true); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs index 423e59b46..f69095eeb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs @@ -113,7 +113,7 @@ namespace Barotrauma.Items.Components forceMultiplier *= MathHelper.Lerp(0.5f, 2.0f, (float)Math.Sqrt(User.GetSkillLevel("helm") / 100)); } - float voltageFactor = MinVoltage <= 0.0f ? 1.0f : Math.Min(Voltage / MinVoltage, 1.0f); + float voltageFactor = MinVoltage <= 0.0f ? 1.0f : Math.Min(Voltage, 1.0f); 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); @@ -121,7 +121,7 @@ namespace Barotrauma.Items.Components UpdatePropellerDamage(deltaTime); float maxChangeSpeed = 0.5f; float modifier = 2; - float noise = currForce.Length() * forceMultiplier * modifier / maxForce; + float noise = MathUtils.NearlyEqual(0.0f, maxForce) ? 0.0f : currForce.Length() * forceMultiplier * modifier / maxForce; float min = Math.Max(1 - maxChangeSpeed, 0); float max = 1 + maxChangeSpeed; UpdateAITargets(Math.Clamp(noise, min, max), deltaTime); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index 9abbc812c..0e8e1d5e9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -482,9 +482,9 @@ namespace Barotrauma.Items.Components return componentElement; } - public override void Load(XElement componentElement, bool usePrefabValues) + public override void Load(XElement componentElement, bool usePrefabValues, IdRemap idRemap) { - base.Load(componentElement, usePrefabValues); + base.Load(componentElement, usePrefabValues, idRemap); savedFabricatedItem = componentElement.GetAttributeString("fabricateditemidentifier", ""); savedTimeUntilReady = componentElement.GetAttributeFloat("savedtimeuntilready", 0.0f); savedRequiredTime = componentElement.GetAttributeFloat("savedrequiredtime", 0.0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/OxygenGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/OxygenGenerator.cs index 78bee5143..8e836bcf1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/OxygenGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/OxygenGenerator.cs @@ -8,11 +8,10 @@ namespace Barotrauma.Items.Components { class OxygenGenerator : Powered { - private float powerDownTimer; - private float generatedAmount; - private List ventList; + //key = vent, float = total volume of the hull the vent is in and the hulls connected to it + private Dictionary ventList; private float totalHullVolume; @@ -49,17 +48,12 @@ namespace Barotrauma.Items.Components Voltage = 1.0f; } - if (item.CurrentHull == null) return; + if (item.CurrentHull == null) { return; } if (Voltage < MinVoltage) { - powerDownTimer += deltaTime; return; } - else - { - powerDownTimer = 0.0f; - } CurrFlow = Math.Min(Voltage, 1.0f) * generatedAmount * 100.0f; @@ -76,24 +70,25 @@ namespace Barotrauma.Items.Components public override void UpdateBroken(float deltaTime, Camera cam) { base.UpdateBroken(deltaTime, cam); - powerDownTimer += deltaTime; CurrFlow = 0.0f; } private void GetVents() { - ventList.Clear(); - + ventList = new Dictionary(); foreach (MapEntity entity in item.linkedTo) { - Item linkedItem = entity as Item; - if (linkedItem == null) continue; + if (!(entity is Item linkedItem)) { continue; } Vent vent = linkedItem.GetComponent(); - if (vent == null) continue; + if (vent?.Item.CurrentHull == null) { continue; } - ventList.Add(vent); - if (linkedItem.CurrentHull != null) totalHullVolume += linkedItem.CurrentHull.Volume; + ventList.Add(vent, 0.0f); + foreach (Hull connectedHull in vent.Item.CurrentHull.GetConnectedHulls(includingThis: true, searchDepth: 10, ignoreClosedGaps: true)) + { + totalHullVolume += connectedHull.Volume; + ventList[vent] += connectedHull.Volume; + } } } @@ -101,18 +96,17 @@ namespace Barotrauma.Items.Components { if (ventList == null) { - ventList = new List(); GetVents(); } - if (!ventList.Any() || totalHullVolume <= 0.0f) return; + if (!ventList.Any() || totalHullVolume <= 0.0f) { return; } - foreach (Vent v in ventList) + foreach (KeyValuePair v in ventList) { - if (v.Item.CurrentHull == null) continue; + if (v.Key?.Item.CurrentHull == null) { continue; } - v.OxygenFlow = deltaOxygen * (v.Item.CurrentHull.Volume / totalHullVolume); - v.IsActive = true; + v.Key.OxygenFlow = deltaOxygen * (v.Value / totalHullVolume); + v.Key.IsActive = true; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs index 9db46cfbe..e7a199447 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs @@ -2,7 +2,9 @@ using Microsoft.Xna.Framework; using System; using System.Globalization; +using System.Linq; using System.Xml.Linq; +using Barotrauma.MapCreatures.Behavior; namespace Barotrauma.Items.Components { @@ -11,10 +13,38 @@ namespace Barotrauma.Items.Components private float flowPercentage; private float maxFlow; - private float? targetLevel; + public float? TargetLevel; + + private bool hijacked; + public bool Hijacked + { + get { return hijacked; } + set + { + if (value == hijacked) { return; } + hijacked = value; +#if SERVER + item.CreateServerEvent(this); +#endif + } + } 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 { @@ -66,12 +96,12 @@ namespace Barotrauma.Items.Components { currFlow = 0.0f; - if (targetLevel != null) + if (TargetLevel != null) { pumpSpeedLockTimer -= deltaTime; float hullPercentage = 0.0f; if (item.CurrentHull != null) { hullPercentage = (item.CurrentHull.WaterVolume / item.CurrentHull.Volume) * 100.0f; } - FlowPercentage = ((float)targetLevel - hullPercentage) * 10.0f; + FlowPercentage = ((float)TargetLevel - hullPercentage) * 10.0f; } currPowerConsumption = powerConsumption * Math.Abs(flowPercentage / 100.0f); @@ -92,14 +122,41 @@ namespace Barotrauma.Items.Components //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) + { + 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 (hull.BallastFlora != null) { return; } + + Vector2 offset = item.WorldPosition - hull.WorldPosition; + hull.BallastFlora = new BallastFloraBehavior(hull, BallastFloraPrefab.Find(identifier), offset, firstGrowth: true); + +#if SERVER + hull.BallastFlora.SendNetworkMessage(hull.BallastFlora, BallastFloraBehavior.NetworkHeader.Spawn); +#endif + } + partial void UpdateProjSpecific(float deltaTime); public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f) { + if (Hijacked) { return; } + if (connection.Name == "toggle") { IsActive = !IsActive; @@ -115,7 +172,7 @@ namespace Barotrauma.Items.Components if (float.TryParse(signal, NumberStyles.Any, CultureInfo.InvariantCulture, out float tempSpeed)) { flowPercentage = MathHelper.Clamp(tempSpeed, -100.0f, 100.0f); - targetLevel = null; + TargetLevel = null; pumpSpeedLockTimer = 0.1f; } } @@ -123,7 +180,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 = MathHelper.Clamp(tempTarget + 50.0f, 0.0f, 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..1c923ba90 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; } @@ -399,6 +399,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 @@ -589,7 +591,7 @@ namespace Barotrauma.Items.Components if (objective.SubObjectives.None()) { int itemCount = item.ContainedItems.Count(i => i != null && container.ContainableItems.Any(ri => ri.MatchesItem(i))) + 1; - AIContainItems(container, character, objective, itemCount, equip: false, removeEmpty: true, spawnItemIfNotFound: character.TeamID == Character.TeamType.FriendlyNPC); + AIContainItems(container, character, objective, itemCount, equip: false, removeEmpty: true, spawnItemIfNotFound: character.TeamID == Character.TeamType.FriendlyNPC, dropItemOnDeselected: true); character.Speak(TextManager.Get("DialogReactorFuel"), null, 0.0f, "reactorfuel", 30.0f); } return false; @@ -604,10 +606,7 @@ namespace Barotrauma.Items.Components { if (item != null && container.ContainableItems.Any(ri => ri.MatchesItem(item))) { - if (!character.Inventory.TryPutItem(item, character, allowedSlots: item.AllowedSlots)) - { - item.Drop(character); - } + item.Drop(character); break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs index 96d1daf63..6e09bdcf2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs @@ -64,6 +64,7 @@ namespace Barotrauma.Items.Components private bool useDirectionalPing = false; private Vector2 pingDirection = new Vector2(1.0f, 0.0f); + private bool useMineralScanner; private bool aiPingCheckPending; @@ -103,6 +104,10 @@ namespace Barotrauma.Items.Components set; } + [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 { get { return zoom; } @@ -343,6 +348,7 @@ namespace Barotrauma.Items.Components bool isActive = msg.ReadBoolean(); bool directionalPing = useDirectionalPing; float zoomT = zoom, pingDirectionT = 0.0f; + bool mineralScanner = useMineralScanner; if (isActive) { zoomT = msg.ReadRangedSingle(0.0f, 1.0f, 8); @@ -351,6 +357,7 @@ namespace Barotrauma.Items.Components { pingDirectionT = msg.ReadRangedSingle(0.0f, 1.0f, 8); } + mineralScanner = msg.ReadBoolean(); } if (!item.CanClientAccess(c)) { return; } @@ -366,9 +373,14 @@ namespace Barotrauma.Items.Components float pingAngle = MathHelper.Lerp(0.0f, MathHelper.TwoPi, pingDirectionT); pingDirection = new Vector2((float)Math.Cos(pingAngle), (float)Math.Sin(pingAngle)); } + useMineralScanner = mineralScanner; #if CLIENT zoomSlider.BarScroll = zoomT; directionalModeSwitch.Selected = useDirectionalPing; + if (mineralScannerSwitch != null) + { + mineralScannerSwitch.Selected = useMineralScanner; + } #endif } #if SERVER @@ -388,6 +400,7 @@ namespace Barotrauma.Items.Components float pingAngle = MathUtils.WrapAngleTwoPi(MathUtils.VectorToAngle(pingDirection)); msg.WriteRangedSingle(MathUtils.InverseLerp(0.0f, MathHelper.TwoPi, pingAngle), 0.0f, 1.0f, 8); } + msg.Write(useMineralScanner); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs index 2576335f2..663e9c815 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs @@ -67,7 +67,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 +90,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 @@ -417,6 +420,7 @@ 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; Vector2 newAvoidStrength = Vector2.Zero; @@ -426,12 +430,26 @@ namespace Barotrauma.Items.Components var closeCells = Level.Loaded.GetCells(controlledSub.WorldPosition, 4); foreach (VoronoiCell cell in closeCells) { + 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); + if (dist > damagingWallAvoidRadius) { continue; } + Vector2 diff = controlledSub.WorldPosition - cell.Center; + Vector2 avoid = Vector2.Normalize(diff) * (damagingWallAvoidRadius - dist) / damagingWallAvoidRadius; + newAvoidStrength += avoid; + debugDrawObstacles.Add(new ObstacleDebugInfo(edge, edge.Center, 1.0f, avoid, cell.Translation)); + } + continue; + } + foreach (GraphEdge edge in cell.Edges) { if (MathUtils.GetLineIntersection(edge.Point1 + cell.Translation, edge.Point2 + cell.Translation, controlledSub.WorldPosition, cell.Center, out Vector2 intersection)) { Vector2 diff = controlledSub.WorldPosition - intersection; - //far enough -> ignore if (Math.Abs(diff.X) > avoidDist.X && Math.Abs(diff.Y) > avoidDist.Y) { @@ -497,6 +515,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; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Vent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Vent.cs index 9559fba08..f4cb519e8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Vent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Vent.cs @@ -13,11 +13,7 @@ namespace Barotrauma.Items.Components set { oxygenFlow = Math.Max(value, 0.0f); } } - public Vent (Item item, XElement element) - : base(item, element) - { - - } + public Vent (Item item, XElement element) : base(item, element) { } public override void Update(float deltaTime, Camera cam) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/NameTag.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/NameTag.cs index 6cd9e1d39..97c8acae8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/NameTag.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/NameTag.cs @@ -1,8 +1,4 @@ -using Barotrauma.Networking; using System.Xml.Linq; -#if CLIENT -using Microsoft.Xna.Framework.Graphics; -#endif namespace Barotrauma.Items.Components { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs index a356035b3..986af5634 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs @@ -55,6 +55,22 @@ namespace Barotrauma.Items.Components set; } + private float extraLoad; + private float extraLoadSetTime; + /// + /// Additional load coming from somewhere else than the devices connected to the junction box (e.g. ballast flora or piezo crystals). + /// Goes back to zero automatically if you stop setting the value. + /// + public float ExtraLoad + { + get { return extraLoad; } + set + { + extraLoad = Math.Max(value, 0.0f); + extraLoadSetTime = (float)Timing.TotalTime; + } + } + //can the component transfer power private bool canTransfer; public bool CanTransfer @@ -135,6 +151,11 @@ namespace Barotrauma.Items.Components { RefreshConnections(); + if (Timing.TotalTime > extraLoadSetTime + 1.0) + { + extraLoad = Math.Max(extraLoad - 1000.0f * deltaTime, 0); + } + if (!CanTransfer) { return; } if (isBroken) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs index 9bff25e5f..344c81281 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs @@ -231,8 +231,16 @@ namespace Barotrauma.Items.Components //and send out a "probe signal" which the PowerTransfer components use to add up the grid power/load foreach (Powered powered in poweredList) { - if (powered is PowerTransfer) { continue; } - if (powered.currPowerConsumption > 0.0f) + if (powered is PowerTransfer pt) + { + if (pt.ExtraLoad > 0.0f) + { + lastPowerProbeRecipients.Clear(); + powered.powerIn?.SendPowerProbeSignal(powered.item, -pt.ExtraLoad); + } + continue; + } + else if (powered.currPowerConsumption > 0.0f) { //consuming power lastPowerProbeRecipients.Clear(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index 88bfb72e0..27108cd50 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -60,14 +60,14 @@ namespace Barotrauma.Items.Components public List IgnoredBodies; - private Character user; + private Character _user; public Character User { - get { return user; } + get { return _user; } set { - user = value; - Attack?.SetUser(user); + _user = value; + Attack?.SetUser(_user); } } @@ -211,7 +211,54 @@ namespace Barotrauma.Items.Components } } - public override bool Use(float deltaTime, Character character = null) + private void Launch(Character user, Vector2 simPosition, float rotation) + { + Item.body.ResetDynamics(); + Item.SetTransform(simPosition, rotation); + // 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)); + } + + public void Shoot(Character user, Vector2 weaponPos, Vector2 spawnPos, float rotation, List ignoredBodies, bool createNetworkEvent) + { + //add the limbs of the shooter to the list of bodies to be ignored + //so that the player can't shoot himself + IgnoredBodies = ignoredBodies; + Vector2 projectilePos = weaponPos; + //make sure there's no obstacles between the base of the weapon (or the shoulder of the character) and the end of the barrel + if (Submarine.PickBody(weaponPos, spawnPos, IgnoredBodies, Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking) == null) + { + //no obstacles -> we can spawn the projectile at the barrel + projectilePos = spawnPos; + } + else if ((weaponPos - spawnPos).LengthSquared() > 0.0001f) + { + //spawn the projectile body.GetMaxExtent() away from the position where the raycast hit the obstacle + Vector2 newPos = weaponPos - Vector2.Normalize(spawnPos - projectilePos) * Math.Max(Item.body.GetMaxExtent(), 0.1f); + if (MathUtils.IsValid(newPos)) + { + projectilePos = newPos; + } + } + Launch(user, projectilePos, rotation); + if (createNetworkEvent && !Item.Removed && GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) + { +#if SERVER + launchRot = rotation; + Item.CreateServerEvent(this, new object[] { true }); //true = indicate that this is a launch event +#endif + } + } + + public bool Use(Character character = null) { if (character != null && !characterUsable) { return false; } @@ -230,16 +277,16 @@ namespace Barotrauma.Items.Components } else { - Launch(launchDir * LaunchImpulse * item.body.Mass); + DoLaunch(launchDir * LaunchImpulse * item.body.Mass); } } - User = character; - return true; } - private void Launch(Vector2 impulse) + public override bool Use(float deltaTime, Character character = null) => Use(character); + + private void DoLaunch(Vector2 impulse) { hits.Clear(); @@ -342,7 +389,15 @@ 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); + } } } } @@ -360,6 +415,7 @@ namespace Barotrauma.Items.Components { //ignore sensors and items 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; } @@ -381,6 +437,7 @@ namespace Barotrauma.Items.Components { //ignore sensors and items if (fixture?.Body == null || fixture.IsSensor) { return -1; } + if (fixture.Body.UserData is VineTile) { return -1; } if (fixture.Body.UserData is Item item && (item.GetComponent() == null && !item.Prefab.DamagedByProjectiles || item.Condition <= 0)) { return -1; } if (fixture.Body?.UserData as string == "ruinroom") { return -1; } @@ -577,20 +634,27 @@ namespace Barotrauma.Items.Components { if (Attack != null) { attackResult = Attack.DoDamage(User, damageable, item.WorldPosition, 1.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) + { + attackResult = Attack.DoDamage(User, destructibleWall, item.WorldPosition, 1.0f); + } + } if (character != null) { character.LastDamageSource = item; } #if CLIENT - PlaySound(ActionType.OnUse, user: user); - PlaySound(ActionType.OnImpact, user: user); + PlaySound(ActionType.OnUse, user: _user); + PlaySound(ActionType.OnImpact, user: _user); #endif if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) { if (target.Body.UserData is Limb targetLimb) { - ApplyStatusEffects(ActionType.OnUse, 1.0f, character, targetLimb, user: user); - ApplyStatusEffects(ActionType.OnImpact, 1.0f, character, targetLimb, user: user); + ApplyStatusEffects(ActionType.OnUse, 1.0f, character, targetLimb, user: _user); + ApplyStatusEffects(ActionType.OnImpact, 1.0f, character, targetLimb, user: _user); var attack = targetLimb.attack; if (attack != null) { @@ -626,8 +690,8 @@ namespace Barotrauma.Items.Components } else { - ApplyStatusEffects(ActionType.OnUse, 1.0f, useTarget: target.Body.UserData as Entity, user: user); - ApplyStatusEffects(ActionType.OnImpact, 1.0f, useTarget: target.Body.UserData as Entity, user: user); + ApplyStatusEffects(ActionType.OnUse, 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) { @@ -703,7 +767,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; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs index 1f32402a8..fd1ad5456 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs @@ -446,7 +446,7 @@ namespace Barotrauma.Items.Components //oxygen generators don't deteriorate if they're not running if (oxyGenerator.CurrFlow > 0.1f) { return true; } } - else if (ic is Powered powered) + else if (ic is Powered powered && !(powered is LightComponent)) { if (powered.Voltage >= powered.MinVoltage) { return true; } } @@ -477,6 +477,7 @@ namespace Barotrauma.Items.Components private void UpdateFixAnimation(Character character) { + if (character == null || character.IsDead || character.IsIncapacitated) { return; } character.AnimController.UpdateUseItem(false, item.WorldPosition + new Vector2(0.0f, 100.0f) * ((item.Condition / item.MaxCondition) % 0.1f)); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs index 2e748b8ed..f10a2540f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs @@ -40,7 +40,7 @@ namespace Barotrauma.Items.Components { get { - if (recipientsDirty) RefreshRecipients(); + if (recipientsDirty) { RefreshRecipients(); } return recipients; } } @@ -61,7 +61,7 @@ namespace Barotrauma.Items.Components return "Connection (" + item.Name + ", " + Name + ")"; } - public Connection(XElement element, ConnectionPanel connectionPanel) + public Connection(XElement element, ConnectionPanel connectionPanel, IdRemap idRemap) { #if CLIENT @@ -150,8 +150,11 @@ namespace Barotrauma.Items.Components if (index == -1) break; int id = subElement.GetAttributeInt("w", 0); - if (id < 0) id = 0; - wireId[index] = (ushort)id; + if (id < 0) + { + id = 0; + } + wireId[index] = idRemap.GetOffsetId(id); break; @@ -162,6 +165,11 @@ namespace Barotrauma.Items.Components } } + public void SetRecipientsDirty() + { + recipientsDirty = true; + } + private void RefreshRecipients() { recipients.Clear(); @@ -304,6 +312,7 @@ namespace Barotrauma.Items.Components { if (wires[i].Item.body != null) wires[i].Item.body.Enabled = false; wires[i].Connect(this, false, false); + wires[i].FixNodeEnds(); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs index 4cabc354f..b3d11592a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs @@ -50,10 +50,10 @@ namespace Barotrauma.Items.Components switch (subElement.Name.ToString()) { case "input": - Connections.Add(new Connection(subElement, this)); + Connections.Add(new Connection(subElement, this, IdRemap.DiscardId)); break; case "output": - Connections.Add(new Connection(subElement, this)); + Connections.Add(new Connection(subElement, this, IdRemap.DiscardId)); break; } } @@ -218,9 +218,9 @@ namespace Barotrauma.Items.Components return false; } - public override void Load(XElement element, bool usePrefabValues) + public override void Load(XElement element, bool usePrefabValues, IdRemap idRemap) { - base.Load(element, usePrefabValues); + base.Load(element, usePrefabValues, idRemap); List loadedConnections = new List(); @@ -229,10 +229,10 @@ namespace Barotrauma.Items.Components switch (subElement.Name.ToString()) { case "input": - loadedConnections.Add(new Connection(subElement, this)); + loadedConnections.Add(new Connection(subElement, this, idRemap)); break; case "output": - loadedConnections.Add(new Connection(subElement, this)); + loadedConnections.Add(new Connection(subElement, this, idRemap)); break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs index 30d7419b2..361be3884 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs @@ -14,12 +14,12 @@ namespace Barotrauma.Items.Components private Color lightColor; private float lightBrightness; private float blinkFrequency; + private float pulseFrequency, pulseAmount; private float range; - private float flicker, flickerState; + private float flicker, flickerSpeed; private bool castShadows; private bool drawBehindSubs; - private float blinkTimer; private double lastToggleSignalTime; @@ -90,14 +90,49 @@ namespace Barotrauma.Items.Components set { flicker = MathHelper.Clamp(value, 0.0f, 1.0f); +#if CLIENT + if (light != null) { light.LightSourceParams.Flicker = flicker; } +#endif } } [Editable, Serialize(1.0f, false, description: "How fast the light flickers.")] public float FlickerSpeed { - get; - set; + get { return flickerSpeed; } + set + { + flickerSpeed = value; +#if CLIENT + if (light != null) { light.LightSourceParams.FlickerSpeed = flickerSpeed; } +#endif + } + } + + [Editable, Serialize(0.0f, true, description: "How rapidly the light pulsates (in Hz). 0 = no blinking.")] + public float PulseFrequency + { + get { return pulseFrequency; } + set + { + pulseFrequency = MathHelper.Clamp(value, 0.0f, 60.0f); +#if CLIENT + if (light != null) { light.LightSourceParams.PulseFrequency = pulseFrequency; } +#endif + } + } + + [Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f, DecimalCount = 2), Serialize(0.0f, true, description: "How much light pulsates (in Hz). 0 = not at all, 1 = alternates between full brightness and off.")] + public float PulseAmount + { + get { return pulseAmount; } + set + { + pulseAmount = MathHelper.Clamp(value, 0.0f, 1.0f); +#if CLIENT + if (light != null) { light.LightSourceParams.PulseAmount = pulseAmount; } +#endif + } } [Editable, Serialize(0.0f, true, description: "How rapidly the light blinks on and off (in Hz). 0 = no blinking.")] @@ -107,6 +142,9 @@ namespace Barotrauma.Items.Components set { blinkFrequency = MathHelper.Clamp(value, 0.0f, 60.0f); +#if CLIENT + if (light != null) { light.LightSourceParams.BlinkFrequency = blinkFrequency; } +#endif } } @@ -161,11 +199,16 @@ namespace Barotrauma.Items.Components { ParentSub = item.CurrentHull?.Submarine, Position = item.Position, - CastShadows = castShadows, + CastShadows = castShadows, IsBackground = drawBehindSubs, SpriteScale = Vector2.One * item.Scale, Range = range }; + light.LightSourceParams.Flicker = flicker; + light.LightSourceParams.FlickerSpeed = FlickerSpeed; + light.LightSourceParams.PulseAmount = pulseAmount; + light.LightSourceParams.PulseFrequency = pulseFrequency; + light.LightSourceParams.BlinkFrequency = blinkFrequency; #endif IsActive = IsOn; @@ -229,22 +272,7 @@ namespace Barotrauma.Items.Components lightBrightness = MathHelper.Lerp(lightBrightness, powerConsumption <= 0.0f ? 1.0f : Math.Min(Voltage, 1.0f), 0.1f); } - if (blinkFrequency > 0.0f) - { - blinkTimer = (blinkTimer + deltaTime * blinkFrequency) % 1.0f; - } - - if (blinkTimer > 0.5f) - { - SetLightSourceState(false, lightBrightness); - } - else - { - flickerState += deltaTime * FlickerSpeed; - flickerState %= 255; - float noise = PerlinNoise.GetPerlin(flickerState, flickerState * 0.5f) * flicker; - SetLightSourceState(true, lightBrightness * (1.0f - noise)); - } + SetLightSourceState(true, lightBrightness); if (powerIn == null && powerConsumption > 0.0f) { Voltage -= deltaTime; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs index 19599da8e..981770226 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs @@ -1,4 +1,5 @@ using Barotrauma.Networking; +using System.Collections.Generic; using System.Xml.Linq; namespace Barotrauma.Items.Components @@ -7,6 +8,10 @@ namespace Barotrauma.Items.Components { private const int MaxMessageLength = ChatMessage.MaxLength; + private const int MaxMessages = 60; + + private List messageHistory = new List(MaxMessages); + public string DisplayedWelcomeMessage { get; @@ -50,5 +55,41 @@ namespace Barotrauma.Items.Components string inputSignal = signal.Replace("\\n", "\n"); ShowOnDisplay(inputSignal); } + + public override void OnItemLoaded() + { + base.OnItemLoaded(); + if (!string.IsNullOrEmpty(DisplayedWelcomeMessage)) + { + ShowOnDisplay(DisplayedWelcomeMessage); + DisplayedWelcomeMessage = ""; + //remove welcome message if a game session is running so it doesn't reappear on successive rounds + if (GameMain.GameSession != null) + { + welcomeMessage = null; + } + } + } + + public override XElement Save(XElement parentElement) + { + var componentElement = base.Save(parentElement); + for (int i = 0; i < messageHistory.Count; i++) + { + componentElement.Add(new XAttribute("msg" + i, messageHistory[i])); + } + return componentElement; + } + + public override void Load(XElement componentElement, bool usePrefabValues, IdRemap idRemap) + { + base.Load(componentElement, usePrefabValues, idRemap); + for (int i = 0; i < MaxMessages; i++) + { + string msg = componentElement.GetAttributeString("msg" + i, null); + if (msg == null) { break; } + ShowOnDisplay(msg); + } + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs index 6b0953bc2..53251fdf4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs @@ -17,7 +17,8 @@ namespace Barotrauma.Items.Components Atan, } - protected float[] receivedSignal = new float[2]; + private float[] receivedSignal = new float[2]; + private float[] timeSinceReceived = new float[2]; [Serialize(FunctionType.Sin, false, description: "Which kind of function to run the input through.", alwaysUseInstanceValues: true)] public FunctionType Function @@ -41,12 +42,25 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { - //reset received signals - receivedSignal[0] = float.NaN; - receivedSignal[1] = float.NaN; + if (Function == FunctionType.Atan) + { + for (int i = 0; i < 2; i++) + { + timeSinceReceived[i] += deltaTime; + if (timeSinceReceived[i] > 0.1f) + { + receivedSignal[i] = float.NaN; + } + } + if (!float.IsNaN(receivedSignal[0]) && !float.IsNaN(receivedSignal[1])) + { + float angle = (float)Math.Atan2(receivedSignal[1], receivedSignal[0]); + if (!UseRadians) { angle = MathHelper.ToDegrees(angle); } + item.SendSignal(0, angle.ToString("G", CultureInfo.InvariantCulture), "signal_out", null); + } + } } - public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0, float signalStrength = 1) { float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float value); @@ -89,17 +103,13 @@ namespace Barotrauma.Items.Components case FunctionType.Atan: if (connection.Name == "signal_in_x") { + timeSinceReceived[0] = 0.0f; float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out receivedSignal[0]); } else if (connection.Name == "signal_in_y") { - float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out receivedSignal[1]); - if (!float.IsNaN(receivedSignal[0]) && !float.IsNaN(receivedSignal[1])) - { - float angle = (float)Math.Atan2(receivedSignal[1], receivedSignal[0]); - if (!UseRadians) { angle = MathHelper.ToDegrees(angle); } - item.SendSignal(0, angle.ToString("G", CultureInfo.InvariantCulture), "signal_out", null); - } + timeSinceReceived[1] = 0.0f; + float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out receivedSignal[1]); } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs index 830441855..3ae59f88d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs @@ -1,4 +1,6 @@ -using System.Xml.Linq; +using Microsoft.Xna.Framework; +using System; +using System.Xml.Linq; namespace Barotrauma.Items.Components { @@ -59,6 +61,12 @@ namespace Barotrauma.Items.Components { item.SendSignal(0, signalOut, "signal_out", null); } + + if (item.CurrentHull != null) + { + int waterPercentage = MathHelper.Clamp((int)Math.Round(item.CurrentHull.WaterPercentage), 0, 100); + item.SendSignal(0, waterPercentage.ToString(), "water_%", null); + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs index f3f8911f4..036ca394f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs @@ -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 344df8079..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 ? @@ -200,14 +209,16 @@ namespace Barotrauma.Items.Components { if (connections[0] != null && connections[0] != newConnection) { - if (Vector2.DistanceSquared(nodes[0], connections[0].Item.Position - (refSub?.HiddenSubPosition ?? Vector2.Zero)) < Vector2.DistanceSquared(nodes[nodes.Count - 1], nodePos)) + if (Vector2.DistanceSquared(nodes[0], connections[0].Item.Position - (refSub?.HiddenSubPosition ?? Vector2.Zero)) < + Vector2.DistanceSquared(nodes[nodes.Count - 1], connections[0].Item.Position - (refSub?.HiddenSubPosition ?? Vector2.Zero))) { newNodeIndex = nodes.Count; } } else if (connections[1] != null && connections[1] != newConnection) { - if (Vector2.DistanceSquared(nodes[0], connections[1].Item.Position - (refSub?.HiddenSubPosition ?? Vector2.Zero)) < Vector2.DistanceSquared(nodes[nodes.Count - 1], nodePos)) + if (Vector2.DistanceSquared(nodes[0], connections[1].Item.Position - (refSub?.HiddenSubPosition ?? Vector2.Zero)) < + Vector2.DistanceSquared(nodes[nodes.Count - 1], connections[1].Item.Position - (refSub?.HiddenSubPosition ?? Vector2.Zero))) { newNodeIndex = nodes.Count; } @@ -236,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) { @@ -620,8 +631,8 @@ namespace Barotrauma.Items.Components { if (connections[i]?.Item != null) { - var pt = connections[i].Item.GetComponent(); - if (pt != null) pt.SetConnectionDirty(connections[i]); + connections[i].Item.GetComponent()?.SetConnectionDirty(connections[i]); + connections[i].SetRecipientsDirty(); } } } @@ -651,17 +662,29 @@ namespace Barotrauma.Items.Components } while (removed); } - private void FixNodeEnds() + public void FixNodeEnds() { - if (connections[0] == null || connections[1] == null || nodes.Count == 0) { return; } + Item item0 = connections[0]?.Item; + Item item1 = connections[1]?.Item; + + if (item0 == null && item1 != null) + { + item0 = Item.ItemList.Find(it => it.GetComponent()?.DisconnectedWires.Contains(this) ?? false); + } + else if (item0 != null && item1 == null) + { + item1 = Item.ItemList.Find(it => it.GetComponent()?.DisconnectedWires.Contains(this) ?? false); + } + + if (item0 == null || item1 == null || nodes.Count == 0) { return; } Vector2 nodePos = nodes[0]; - Submarine refSub = connections[0].Item.Submarine ?? connections[1].Item.Submarine; + Submarine refSub = item0.Submarine ?? item1.Submarine; if (refSub != null) { nodePos += refSub.HiddenSubPosition; } - float dist1 = Vector2.DistanceSquared(connections[0].Item.Position, nodePos); - float dist2 = Vector2.DistanceSquared(connections[1].Item.Position, nodePos); + float dist1 = Vector2.DistanceSquared(item0.Position, nodePos); + float dist2 = Vector2.DistanceSquared(item1.Position, nodePos); //first node is closer to the second item //= the nodes are "backwards", need to reverse them @@ -721,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 : @@ -750,9 +778,9 @@ namespace Barotrauma.Items.Components UpdateSections(); } - public override void Load(XElement componentElement, bool usePrefabValues) + public override void Load(XElement componentElement, bool usePrefabValues, IdRemap idRemap) { - base.Load(componentElement, usePrefabValues); + base.Load(componentElement, usePrefabValues, idRemap); string nodeString = componentElement.GetAttributeString("nodes", ""); if (nodeString == "") return; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index 2f91a277f..c86f55e85 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,20 +251,17 @@ 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), 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(); + base.OnMapLoaded(); var lightComponents = item.GetComponents(); if (lightComponents != null && lightComponents.Count() > 0) { @@ -277,6 +275,9 @@ namespace Barotrauma.Items.Components } #endif } + if (loadedRotationLimits.HasValue) { RotationLimits = loadedRotationLimits.Value; } + if (loadedBaseRotation.HasValue) { BaseRotation = loadedBaseRotation.Value; } + UpdateTransformedBarrelPos(); } public override void Update(float deltaTime, Camera cam) @@ -284,7 +285,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(); } @@ -328,19 +329,42 @@ namespace Barotrauma.Items.Components user.WorldPosition + 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; @@ -804,7 +828,7 @@ namespace Barotrauma.Items.Components } if (objective.SubObjectives.None()) { - var loadItemsObjective = AIContainItems(container, character, objective, usableProjectileCount + 1, equip: true, removeEmpty: true); + var loadItemsObjective = AIContainItems(container, character, objective, usableProjectileCount + 1, equip: true, removeEmpty: true, dropItemOnDeselected: true); if (loadItemsObjective == null) { if (usableProjectileCount == 0) @@ -976,6 +1000,8 @@ namespace Barotrauma.Items.Components public override void FlipX(bool relativeToSub) { + BaseRotation = MathHelper.ToDegrees(MathUtils.WrapAngleTwoPi(MathHelper.ToRadians(-BaseRotation))); + minRotation = MathHelper.Pi - minRotation; maxRotation = MathHelper.Pi - maxRotation; @@ -997,7 +1023,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(); } @@ -1041,6 +1082,25 @@ 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(); + 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/Components/Wearable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs index 91f0724a6..3a6743bd7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs @@ -441,9 +441,9 @@ namespace Barotrauma.Items.Components } private int loadedVariant = -1; - public override void Load(XElement componentElement, bool usePrefabValues) + public override void Load(XElement componentElement, bool usePrefabValues, IdRemap idRemap) { - base.Load(componentElement, usePrefabValues); + base.Load(componentElement, usePrefabValues, idRemap); loadedVariant = componentElement.GetAttributeInt("variant", -1); } public override void OnItemLoaded() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 9459bc8cb..ec69d183f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -11,6 +11,8 @@ using System.Globalization; using System.Linq; using System.Xml.Linq; using Barotrauma.Extensions; +using Barotrauma.MapCreatures.Behavior; + #if CLIENT using Microsoft.Xna.Framework.Graphics; #endif @@ -27,7 +29,7 @@ namespace Barotrauma private HashSet tags; - private bool isWire; + private bool isWire, isLogic; private Hull currentHull; public Hull CurrentHull @@ -409,11 +411,9 @@ 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; } float prev = condition; bool wasInFullCondition = IsFullCondition; @@ -426,7 +426,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); } @@ -626,6 +626,8 @@ namespace Barotrauma get { return Prefab.Linkable; } } + public BallastFloraBranch Infector { get; set; } + public override string ToString() { #if CLIENT @@ -640,14 +642,15 @@ namespace Barotrauma { get { return allPropertyObjects; } } + - public Item(ItemPrefab itemPrefab, Vector2 position, Submarine submarine) + public Item(ItemPrefab itemPrefab, Vector2 position, Submarine submarine, ushort id = Entity.NullEntityID) : this(new Rectangle( (int)(position.X - itemPrefab.sprite.size.X / 2 * itemPrefab.Scale), (int)(position.Y + itemPrefab.sprite.size.Y / 2 * itemPrefab.Scale), (int)(itemPrefab.sprite.size.X * itemPrefab.Scale), (int)(itemPrefab.sprite.size.Y * itemPrefab.Scale)), - itemPrefab, submarine) + itemPrefab, submarine, id: id) { } @@ -656,8 +659,8 @@ namespace Barotrauma /// Creates a new item /// /// Should the OnItemLoaded methods of the ItemComponents be called. Use false if the item needs additional initialization before it can be considered fully loaded (e.g. when loading an item from a sub file or cloning an item). - public Item(Rectangle newRect, ItemPrefab itemPrefab, Submarine submarine, bool callOnItemLoaded = true) - : base(itemPrefab, submarine) + public Item(Rectangle newRect, ItemPrefab itemPrefab, Submarine submarine, bool callOnItemLoaded = true, ushort id = Entity.NullEntityID) + : base(itemPrefab, submarine, id) { spriteColor = prefab.SpriteColor; @@ -737,6 +740,8 @@ namespace Barotrauma case "upgrademodule": case "upgradeoverride": case "minimapicon": + case "infectedsprite": + case "damagedinfectedsprite": break; case "staticbody": StaticBodyConfig = subElement; @@ -834,7 +839,8 @@ namespace Barotrauma DebugConsole.Log("Created " + Name + " (" + ID + ")"); - if (Components.All(ic => ic is Wire || ic is Holdable)) { isWire = true; } + if (Components.Any() && Components.All(ic => ic is Wire || ic is Holdable)) { isWire = true; } + if (HasTag("logic")) { isLogic = true; } } partial void InitProjSpecific(); @@ -1172,6 +1178,23 @@ namespace Barotrauma return rootContainer; } + + /// + /// Is the item or any of its containers of the item set to be ignored? + /// + public bool IsThisOrAnyContainerIgnoredByAI() + { + if (IgnoreByAI) { return true; } + if (Container == null) { return false; } + if (Container.IgnoreByAI) { return true; } + var container = Container; + while (container.Container != null) + { + container = container.Container; + if (container.IgnoreByAI) { return true; } + } + return false; + } public bool IsOwnedBy(Entity entity) => FindParentInventory(i => i.Owner == entity) != null; @@ -1419,6 +1442,7 @@ namespace Barotrauma if (!isActive) { return; } ApplyStatusEffects(ActionType.Always, deltaTime, character: (parentInventory as CharacterInventory)?.Owner as Character); + ApplyStatusEffects(parentInventory == null ? ActionType.OnNotContained : ActionType.OnContained, deltaTime, character: (parentInventory as CharacterInventory)?.Owner as Character); for (int i = 0; i < updateableComponents.Count; i++) { @@ -1451,8 +1475,6 @@ namespace Barotrauma #endif ic.WasUsed = false; - ic.ApplyStatusEffects(parentInventory == null ? ActionType.OnNotContained : ActionType.OnContained, deltaTime, character: (parentInventory as CharacterInventory)?.Owner as Character); - if (ic.IsActive) { if (condition <= 0.0f) @@ -1884,11 +1906,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 _); @@ -2481,9 +2498,9 @@ namespace Barotrauma partial void UpdateNetPosition(float deltaTime); - public static Item Load(XElement element, Submarine submarine) + public static Item Load(XElement element, Submarine submarine, IdRemap idRemap) { - return Load(element, submarine, createNetworkEvent: false); + return Load(element, submarine, createNetworkEvent: false, idRemap: idRemap); } /// @@ -2493,7 +2510,7 @@ namespace Barotrauma /// The submarine to spawn the item in (can be null) /// Should an EntitySpawner event be created to notify clients about the item being created. /// - public static Item Load(XElement element, Submarine submarine, bool createNetworkEvent) + public static Item Load(XElement element, Submarine submarine, bool createNetworkEvent, IdRemap idRemap) { string name = element.Attribute("name").Value; string identifier = element.GetAttributeString("identifier", ""); @@ -2512,13 +2529,11 @@ namespace Barotrauma rect.Height = (int)(prefab.Size.Y * prefab.Scale); } - Item item = new Item(rect, prefab, submarine, callOnItemLoaded: false) + Item item = new Item(rect, prefab, submarine, callOnItemLoaded: false, id: idRemap.GetOffsetId(element)) { Submarine = submarine, - ID = (ushort)int.Parse(element.Attribute("ID").Value), linkedToID = new List() }; - item.OriginalID = item.ID; #if SERVER if (createNetworkEvent) @@ -2543,15 +2558,7 @@ namespace Barotrauma if (shouldBeLoaded) { property.TrySetValue(item, attribute.Value); } } - string linkedToString = element.GetAttributeString("linked", ""); - if (linkedToString != "") - { - string[] linkedToIds = linkedToString.Split(','); - for (int i = 0; i < linkedToIds.Length; i++) - { - item.linkedToID.Add((ushort)int.Parse(linkedToIds[i])); - } - } + item.ParseLinks(element, idRemap); bool thisIsOverride = element.GetAttributeBool("isoverride", false); @@ -2583,7 +2590,7 @@ namespace Barotrauma { ItemComponent component = unloadedComponents.Find(x => x.Name == subElement.Name.ToString()); if (component == null) { continue; } - component.Load(subElement, usePrefabValues); + component.Load(subElement, usePrefabValues, idRemap); unloadedComponents.Remove(component); break; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index 0bbe1b03f..8ddbd9ed0 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 { @@ -465,12 +466,33 @@ 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; } [Serialize(true, false)] public bool CanFlipY { get; private set; } + + [Serialize(false, false)] + public bool IsDangerous { get; private set; } public bool CanSpriteFlipX { get; private set; } @@ -594,7 +616,6 @@ namespace Barotrauma originalName = element.GetAttributeString("name", ""); name = originalName; identifier = element.GetAttributeString("identifier", ""); - if (!Enum.TryParse(element.GetAttributeString("category", "Misc"), true, out MapEntityCategory category)) { category = MapEntityCategory.Misc; @@ -648,7 +669,7 @@ namespace Barotrauma if (string.IsNullOrEmpty(name)) { - DebugConsole.ThrowError($"Unnamed item ({identifier})in {filePath}!"); + DebugConsole.ThrowError($"Unnamed item ({identifier}) in {filePath}!"); } DebugConsole.Log(" " + name); @@ -771,6 +792,28 @@ namespace Barotrauma MinimapIcon = new Sprite(subElement, iconFolder, lazyLoad: true); } break; + case "infectedsprite": + { + string iconFolder = ""; + if (!subElement.GetAttributeString("texture", "").Contains("/")) + { + iconFolder = Path.GetDirectoryName(filePath); + } + + InfectedSprite = new Sprite(subElement, iconFolder, lazyLoad: true); + } + break; + case "damagedinfectedsprite": + { + string iconFolder = ""; + if (!subElement.GetAttributeString("texture", "").Contains("/")) + { + iconFolder = Path.GetDirectoryName(filePath); + } + + DamagedInfectedSprite = new Sprite(subElement, iconFolder, lazyLoad: true); + } + break; case "brokensprite": string brokenSpriteFolder = ""; if (!subElement.GetAttributeString("texture", "").Contains("/")) @@ -874,12 +917,25 @@ namespace Barotrauma break; case "levelresource": - foreach (XElement levelCommonnessElement in subElement.Elements()) + foreach (XElement levelCommonnessElement in subElement.GetChildElements("commonness")) { - string levelName = levelCommonnessElement.GetAttributeString("levelname", "").ToLowerInvariant(); - if (!LevelCommonness.ContainsKey(levelName)) + string levelName = levelCommonnessElement.GetAttributeString("leveltype", "").ToLowerInvariant(); + 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; @@ -927,6 +983,16 @@ namespace Barotrauma "Item prefab \"" + name + "\" has no identifier. All item prefabs have a unique identifier string that's used to differentiate between items during saving and loading."); } +#if DEBUG + if (!Category.HasFlag(MapEntityCategory.Legacy) && !HideInMenus) + { + if (!string.IsNullOrEmpty(originalName)) + { + DebugConsole.AddWarning($"Item \"{(string.IsNullOrEmpty(identifier) ? name : identifier)}\" has a hard-coded name, and won't be localized to other languages."); + } + } +#endif + AllowedLinks = element.GetAttributeStringArray("allowedlinks", new string[0], convertToLowerInvariant: true).ToList(); Prefabs.Add(this, allowOverriding); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs new file mode 100644 index 000000000..41c935e8b --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs @@ -0,0 +1,1105 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Xml.Linq; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; +using FarseerPhysics; +using FarseerPhysics.Dynamics; +using Microsoft.Xna.Framework; + +namespace Barotrauma.MapCreatures.Behavior +{ + class BallastFloraBranch : VineTile + { + public readonly BallastFloraBehavior? ParentBallastFlora; + public int ID = -1; + + public ushort ClaimedItem; + public bool HasClaimedItem; + + public float MaxHealth = 100f; + public float Health = 100f; + + public bool SpawningItem; + public Item? AttackItem; + + public bool IsRoot; + public bool Removed; + + public Hull? CurrentHull; + + public float Pulse = 1.0f; + private bool inflate; + private float pulseDelay = Rand.Range(0f, 3f); + + public float AccumulatedDamage; + + // Adjacent tiles, used to free up sides when this branch gets removed + public readonly Dictionary Connections = new Dictionary(); + + public BallastFloraBranch(BallastFloraBehavior? parent, Vector2 position, VineTileType type, FoliageConfig? flowerConfig = null, FoliageConfig? leafConfig = null, Rectangle? rect = null) + : base(null, position, type, flowerConfig, leafConfig, rect) + { + ParentBallastFlora = parent; + } + + public void UpdateHealth() + { + if (MaxHealth <= Health) { return; } + Color healthColor = Color.White * (1.0f - Health / MaxHealth); + HealthColor = healthColor; + } + + public void UpdatePulse(float deltaTime, float inflateSpeed, float deflateSpeed, float delay) + { + if (ParentBallastFlora == null) { return; } + + if (pulseDelay > 0) + { + pulseDelay -= deltaTime; + return; + } + + if (inflate) + { + Pulse += inflateSpeed * deltaTime; + + if (Pulse > 1.25f) + { + inflate = false; + } + } + else + { + Pulse -= deflateSpeed * deltaTime; + if (Pulse < 1f) + { + inflate = true; + pulseDelay = delay; + } + } + } + } + + internal partial class BallastFloraBehavior : ISerializableEntity + { +#if DEBUG || UNSTABLE + public List> debugSearchLines = new List>(); +#endif + + public enum NetworkHeader + { + Spawn, + Kill, + BranchCreate, + BranchRemove, + BranchDamage, + Infect + } + + public enum AttackType + { + Fire, + Explosives, + Other + } + + public struct AITarget + { + public string[] Tags; + public int Priority; + + public AITarget(XElement element) + { + Tags = element.GetAttributeStringArray("tags", new string[0]); + Priority = element.GetAttributeInt("priority", 0); + } + + public bool Matches(Item item) + { + foreach (string tag in item.GetTags()) + { + foreach (string targetTag in Tags) + { + if (tag == targetTag) { return true; } + } + } + + return false; + } + } + + [Serialize(0.25f, true, "Scale of the branches")] + public float BaseBranchScale { get; set; } + + [Serialize(0.25f, true, "Scale of the flowers")] + public float BaseFlowerScale { get; set; } + + [Serialize(0.5f, true, "Scale of the leaves")] + public float BaseLeafScale { get; set; } + + [Serialize(0.33f, true, "Chance for a flower to appear on the branch")] + public float FlowerProbability { get; set; } + + [Serialize(0.7f, true, "Change for leaves to appear for the branch")] + public float LeafProbability { get; set; } + + [Serialize(3f, true, "Delay between pulses")] + public float PulseDelay { get; set; } + + [Serialize(3f, true, "How fast the flower inflates during a pulse")] + public float PulseInflateSpeed { get; set; } + + [Serialize(1f, true, "How fast the flower deflates")] + public float PulseDeflateSpeed { get; set; } + + [Serialize(32, true, "How many vines must grow before the plant breaks thru the wall")] + public int BreakthroughPoint { get; set; } + + [Serialize(false, true, "Has the plant grown large enough to expose itself")] + public bool HasBrokenThrough { get; set; } + + [Serialize(300, true, "How far the ballast flora can detect items")] + public int Sight { get; set; } + + [Serialize(100, true, "How much health the branches have")] + public int BranchHealth { get; set; } + + [Serialize(400, true, "How much health the stem has")] + public int StemHealth { get; set; } + + [Serialize(300f, true, "How much power the ballast flora takes from junction boxes")] + public float PowerConsumptionMin { get; set; } + + [Serialize(3000f, true, "How much the power drain spikes")] + public float PowerConsumptionMax { get; set; } + + [Serialize(10f, true, "How long it takes for power drain to wind down")] + public float PowerConsumptionDuration { get; set; } + + [Serialize(250f, true, "How much power does it take to accelerate growth")] + public float PowerRequirement { get; set; } + + [Serialize(5f, true, "Maximum anger, anger increases when the plant gets damaged and increases growth speed")] + public float MaxAnger { get; set; } + + [Serialize(10000f, true, "Maximum power buffer")] + public float MaxPowerCapacity { get; set; } + + [Serialize("", true, "Item prefab that is spawned when threatened")] + public string AttackItemPrefab { get; set; } = ""; + + [Serialize(0.8f, true, "How resistant the ballast flora is to exlposives before it blooms")] + public float ExplosionResistance { get; set; } + + [Serialize(5f, true, "How much damage is taken from open fires")] + public float FireVulnerability { 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; + + [Serialize(0f, true, "How much power the ballast flora has stored.")] + public float AvailablePower + { + get => availablePower; + set => availablePower = Math.Max(value, MaxPowerCapacity); + } + + private float anger; + + [Serialize(1f, true, "How enraged the flora is, affects how fast it grows.")] + public float Anger + { + get => anger; + set => anger = Math.Clamp(value, 1f, MaxAnger); + } + + public string Name { get; } = ""; + + public Hull Parent { get; private set; } + + public BallastFloraPrefab Prefab { get; private set; } + + public Dictionary SerializableProperties { get; private set; } + + public Vector2 Offset; + + public readonly List ClaimedTargets = new List(); + public readonly List ClaimedJunctionBoxes = new List(); + public readonly List ClaimedBatteries = new List(); + public readonly Dictionary IgnoredTargets = new Dictionary(); + + private readonly List> tempClaimedTargets = new List>(); + + private int flowerVariants, leafVariants; + public readonly List Targets = new List(); + + public float PowerConsumptionTimer; + + private float defenseCooldown, toxinsCooldown, fireCheckCooldown; + private float damageIndicatorTimer, selfDamageTimer, toxinsTimer; + + private readonly List branchesVulnerableToFire = new List(); + + public readonly List Branches = new List(); + private readonly List bodies = new List(); + + public readonly BallastFloraStateMachine StateMachine; + + public int GrowthWarps; + + public void OnMapLoaded() + { + foreach ((ushort itemId, int branchid) in tempClaimedTargets) + { + if (Entity.FindEntityByID(itemId) is Item item) + { + ClaimTarget(item, Branches.FirstOrDefault(b => b.ID == branchid), true); + } + } + + foreach (BallastFloraBranch branch in Branches) + { + UpdateConnections(branch); + CreateBody(branch); + } + } + + + private int CreateID() + { + int maxId = Branches.Any() ? Branches.Max(b => b.ID) : 0; + return ++maxId; + } + + public Vector2 GetWorldPosition() + { + return Parent.WorldPosition + Offset; + } + + public BallastFloraBehavior(Hull parent, BallastFloraPrefab prefab, Vector2 offset, bool firstGrowth = false) + { + Prefab = prefab; + Offset = offset; + Parent = parent; + SerializableProperties = SerializableProperty.DeserializeProperties(this, prefab.Element); + LoadPrefab(prefab.Element); + StateMachine = new BallastFloraStateMachine(this); + if (firstGrowth) { GenerateStem(); } + } + + partial void LoadPrefab(XElement element); + + public void LoadTargets(XElement element) + { + foreach (XElement subElement in element.Elements()) + { + Targets.Add(new AITarget(subElement)); + } + } + + public void Save(XElement element) + { + XElement saveElement = new XElement(nameof(BallastFloraBehavior), + new XAttribute("identifier", Prefab.Identifier), + new XAttribute("offset", XMLExtensions.Vector2ToString(Offset))); + + SerializableProperty.SerializeProperties(this, saveElement); + + foreach (BallastFloraBranch branch in Branches) + { + XElement be = new XElement("Branch", + new XAttribute("flowerconfig", branch.FlowerConfig.Serialize()), + new XAttribute("leafconfig", branch.LeafConfig.Serialize()), + new XAttribute("pos", XMLExtensions.Vector2ToString(branch.Position)), + new XAttribute("ID", branch.ID), + new XAttribute("isroot", branch.IsRoot), + new XAttribute("health", branch.Health.ToString("G", CultureInfo.InvariantCulture)), + new XAttribute("maxhealth", branch.MaxHealth.ToString("G", CultureInfo.InvariantCulture)), + new XAttribute("sides", (int)branch.Sides), + new XAttribute("blockedsides", (int)branch.BlockedSides)); + + if (branch.HasClaimedItem) + { + be.Add(new XAttribute("claimed", (int)branch.ClaimedItem)); + } + + saveElement.Add(be); + } + + foreach (Item target in ClaimedTargets) + { + XElement te = new XElement("ClaimedTarget", new XAttribute("id", target.ID), new XAttribute("branchId", target.Infector.ID)); + saveElement.Add(te); + } + + element.Add(saveElement); + } + + public void LoadSave(XElement element) + { + SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + Offset = element.GetAttributeVector2("offset", Vector2.Zero); + foreach (XElement subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "branch": + LoadBranch(subElement); + break; + + case "claimedtarget": + int id = subElement.GetAttributeInt("id", -1); + int branchId = subElement.GetAttributeInt("branchId", -1); + if (id > 0) + { + tempClaimedTargets.Add(Tuple.Create((UInt16)id, branchId)); + } + break; + } + } + + void LoadBranch(XElement branchElement) + { + Vector2 pos = branchElement.GetAttributeVector2("pos", Vector2.Zero); + bool isRoot = branchElement.GetAttributeBool("isroot", false); + int flowerConfig = getInt("flowerconfig"); + int leafconfig = getInt("leafconfig"); + int id = getInt("ID"); + float health = getFloat("health"); + float maxhealth = getFloat("maxhealth"); + int sides = getInt("sides"); + int blockedSides = getInt("blockedsides"); + int claimedId = branchElement.GetAttributeInt("claimed", -1); + + BallastFloraBranch newBranch = new BallastFloraBranch(this, pos, VineTileType.CrossJunction, FoliageConfig.Deserialize(flowerConfig), FoliageConfig.Deserialize(leafconfig)) + { + ID = id, + Health = health, + MaxHealth = maxhealth, + Sides = (TileSide) sides, + BlockedSides = (TileSide) blockedSides, + IsRoot = isRoot + }; + + if (claimedId > -1) + { + newBranch.HasClaimedItem = true; + newBranch.ClaimedItem = (ushort) claimedId; + } + + Branches.Add(newBranch); + + int getInt(string name) => branchElement.GetAttributeInt(name, 0); + float getFloat(string name) => branchElement.GetAttributeFloat(name, 0f); + } + } + + public void Update(float deltaTime) + { + foreach (BallastFloraBranch branch in Branches) + { + branch.UpdateScale(deltaTime); + branch.UpdatePulse(deltaTime, PulseInflateSpeed, PulseDeflateSpeed, PulseDelay); +#if CLIENT + branch.UpdateHealth(); +#endif + } + + if (damageIndicatorTimer <= 0) + { + foreach (BallastFloraBranch branch in Branches) + { + if (branch.AccumulatedDamage > 0) + { + +#if CLIENT + CreateDamageParticle(branch, branch.AccumulatedDamage); + + if (GameMain.DebugDraw) + { + GUI.AddMessage($"{(int)branch.AccumulatedDamage}", GUI.Style.Red, GetWorldPosition() + branch.Position, Vector2.UnitY * 10.0f, 3f, playSound: false); + } +#elif SERVER + SendNetworkMessage(this, NetworkHeader.BranchDamage, branch, branch.AccumulatedDamage); +#endif + } + + branch.AccumulatedDamage = 0f; + } + + damageIndicatorTimer = 1f; + } + + damageIndicatorTimer -= deltaTime; + + UpdatePowerDrain(deltaTime); + + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } + + StateMachine.Update(deltaTime); + + if (HasBrokenThrough) + { + // I wasn't 100% sure what the performance impact on this so I decide to limit it to only check every 5 seconds + if (fireCheckCooldown <= 0) + { + UpdateFireSources(); + fireCheckCooldown = 5f; + } + else + { + fireCheckCooldown -= deltaTime; + } + + foreach (BallastFloraBranch branch in branchesVulnerableToFire) + { + if (!branch.Removed) + { + DamageBranch(branch, FireVulnerability * deltaTime, AttackType.Fire, null); + } + } + } + + UpdateSelfDamage(deltaTime); + + if (Anger > 1f) + { + Anger -= deltaTime; + } + + // This entire scope is probably very heavy for GC, need to experiment + if (toxinsTimer > 0.1f) + { + if (!string.IsNullOrWhiteSpace(AttackItemPrefab)) + { + Dictionary> branches = new Dictionary>(); + foreach (BallastFloraBranch branch in Branches) + { + if (branch.CurrentHull == null || branch.FlowerConfig.Variant < 0) { continue; } + + if (branches.TryGetValue(branch.CurrentHull, out List? list)) + { + list.Add(branch); + } + else + { + branches.Add(branch.CurrentHull, new List { branch }); + } + } + + foreach (Hull hull in branches.Keys) + { + List list = branches[hull]; + if (!list.Any(HasAcidEmitter)) + { + BallastFloraBranch randomBranh = branches[hull].GetRandom(); + randomBranh.SpawningItem = true; + + ItemPrefab prefab = ItemPrefab.Find(null, AttackItemPrefab); + Entity.Spawner?.AddToSpawnQueue(prefab, Parent.Position + Offset + randomBranh.Position, Parent.Submarine, null, item => + { + randomBranh.AttackItem = item; + randomBranh.SpawningItem = false; + }); + } + + static bool HasAcidEmitter(BallastFloraBranch b) => b.SpawningItem || (b.AttackItem != null && !b.AttackItem.Removed); + } + } + + toxinsTimer -= deltaTime; + } + + if (defenseCooldown >= 0) + { + defenseCooldown -= deltaTime; + } + + if (toxinsCooldown >= 0) + { + toxinsCooldown -= deltaTime; + } + } + + 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; + if (PowerConsumptionTimer > PowerConsumptionDuration) + { + PowerConsumptionTimer = 0f; + } + + float powerConsumption = MathHelper.Lerp(PowerConsumptionMax, PowerConsumptionMin, PowerConsumptionTimer / PowerConsumptionDuration); + float powerDelta = powerConsumption * deltaTime; + + foreach (PowerTransfer jb in ClaimedJunctionBoxes) + { + if (jb.ExtraLoad > Math.Max(PowerConsumptionMin, PowerConsumptionMax)) { continue; } + + jb.ExtraLoad = powerConsumption; + + float currPowerConsumption = -jb.CurrPowerConsumption; + + if (currPowerConsumption > powerDelta) + { + AvailablePower += powerDelta; + } + else + { + AvailablePower += currPowerConsumption * deltaTime; + } + } + + float batteryDrain = powerDelta * 0.1f; + foreach (PowerContainer battery in ClaimedBatteries) + { + float amount = Math.Min(battery.MaxOutPut, batteryDrain); + + if (battery.Charge > amount) + { + battery.Charge -= amount; + AvailablePower += amount; + } + } + } + + /// + /// Update which branches are currently in range of fires + /// + private void UpdateFireSources() + { + branchesVulnerableToFire.Clear(); + foreach (BallastFloraBranch branch in Branches) + { + if (branch.CurrentHull == null) { continue; } + + foreach (FireSource source in branch.CurrentHull.FireSources) + { + if (source.IsInDamageRange(GetWorldPosition() + branch.Position, source.DamageRange)) + { + branchesVulnerableToFire.Add(branch); + } + } + } + } + + private bool IsInWater(BallastFloraBranch branch) + { + if (branch.CurrentHull == null) { return false; } + + float surfaceY = branch.CurrentHull.Surface; + Vector2 pos = Parent.Position + Offset + branch.Position; + return Parent.WaterVolume > 0.0f && pos.Y < surfaceY; + } + + // could probably be moved to the branch constructor + private void SetHull(BallastFloraBranch branch) + { + branch.CurrentHull = Hull.FindHull(GetWorldPosition() + branch.Position, Parent, true); + } + + private void GenerateStem() + { + BallastFloraBranch stem = new BallastFloraBranch(this, Vector2.Zero, VineTileType.Stem, FoliageConfig.EmptyConfig, FoliageConfig.EmptyConfig) + { + BlockedSides = TileSide.Bottom | TileSide.Left | TileSide.Right, + GrowthStep = 1f, + Health = StemHealth, + MaxHealth = StemHealth, + IsRoot = true, + CurrentHull = Parent + }; + + Branches.Add(stem); + CreateBody(stem); + } + + public float GetGrowthSpeed(float deltaTime) + { + float load = PowerRequirement * Anger * deltaTime; + + if (AvailablePower > load) + { + AvailablePower -= load; + return Anger * 2f * deltaTime; + } + + return deltaTime; + } + + public bool TryGrowBranch(BallastFloraBranch parent, TileSide side, out List result) + { + result = new List(); + if (parent.IsSideBlocked(side)) { return false; } + + Vector2 pos = parent.AdjacentPositions[side]; + Rectangle rect = VineTile.CreatePlantRect(pos); + + if (CollidesWithWorld(rect)) + { + parent.BlockedSides |= side; + parent.FailedGrowthAttempts++; + return false; + } + + FoliageConfig flowerConfig = FoliageConfig.EmptyConfig; + FoliageConfig leafConfig = FoliageConfig.EmptyConfig; + + if (FlowerProbability > Rand.Range(0d, 1.0d)) + { + flowerConfig = FoliageConfig.CreateRandomConfig(flowerVariants, 0.5f, 1.0f); + } + + if (LeafProbability > Rand.Range(0d, 1.0d)) + { + leafConfig = FoliageConfig.CreateRandomConfig(leafVariants, 0.5f, 1.0f); + } + + BallastFloraBranch newBranch = new BallastFloraBranch(this, pos, VineTileType.CrossJunction, flowerConfig, leafConfig, rect) + { + ID = CreateID(), + Health = BranchHealth, + MaxHealth = BranchHealth + }; + + SetHull(newBranch); + + if (newBranch.CurrentHull == null || newBranch.CurrentHull.Submarine != Parent.Submarine) + { + parent.BlockedSides |= side; + parent.FailedGrowthAttempts++; + return false; + } + + UpdateConnections(newBranch, parent); + + Branches.Add(newBranch); + result.Add(newBranch); + + OnBranchGrowthSuccess(newBranch); + + if (GrowthWarps > 0) + { + GrowthWarps--; + } + +#if SERVER + SendNetworkMessage(this, NetworkHeader.BranchCreate, newBranch, parent.ID); +#endif + return true; + } + + public bool BranchContainsTarget(BallastFloraBranch branch, Item target) + { + Rectangle worldRect = branch.Rect; + worldRect.Location = GetWorldPosition().ToPoint() + worldRect.Location; + return worldRect.IntersectsWorld(target.WorldRect); + } + + public void ClaimTarget(Item target, BallastFloraBranch? branch, bool load = false) + { + target.Infector = branch; + + if (target.GetComponent() is { } powerTransfer) + { + ClaimedJunctionBoxes.Add(powerTransfer); + } + + if (target.GetComponent() is { } powerContainer) + { + ClaimedBatteries.Add(powerContainer); + } + + ClaimedTargets.Add(target); + + if (branch != null) + { + branch.ClaimedItem = target.ID; + branch.HasClaimedItem = true; + } + +#if SERVER + if (!load) + { + SendNetworkMessage(this, NetworkHeader.Infect, target.ID, true, branch); + } +#endif + } + + private void UpdateConnections(BallastFloraBranch branch, BallastFloraBranch? parent = null) + { + foreach (BallastFloraBranch otherBranch in Branches) + { + var (distX, distY) = branch.Position - otherBranch.Position; + int absDistX = (int) Math.Abs(distX), absDistY = (int) Math.Abs(distY); + + if (absDistX > branch.Rect.Width || absDistY > branch.Rect.Height || absDistX > 0 && absDistY > 0) { continue; } + + TileSide connectingSide = absDistX > absDistY ? distX > 0 ? TileSide.Right : TileSide.Left : distY > 0 ? TileSide.Top : TileSide.Bottom; + + TileSide oppositeSide = connectingSide.GetOppositeSide(); + + if (parent != null) + { + if (otherBranch.BlockedSides.IsBitSet(connectingSide)) + { + branch.BlockedSides |= oppositeSide; + continue; + } + + if (otherBranch != parent) + { + otherBranch.BlockedSides |= connectingSide; + branch.BlockedSides |= oppositeSide; + } + else + { + otherBranch.Sides |= connectingSide; + branch.Sides |= oppositeSide; + } + } + + branch.Connections.TryAdd(oppositeSide, otherBranch); + otherBranch.Connections.TryAdd(connectingSide, branch); + } + } + + private void OnBranchGrowthSuccess(BallastFloraBranch newBranch) + { + if (!HasBrokenThrough) + { + if (Branches.Count > BreakthroughPoint) + { + BreakThrough(); + } + +#if CLIENT + if (newBranch.FlowerConfig.Variant > -1) + { + Vector2 flowerPos = GetWorldPosition() + newBranch.Position; + CreateShapnel(flowerPos); + newBranch.GrowthStep = 2.0f; + SoundPlayer.PlayDamageSound(BurstSound, 1.0f, flowerPos, range: 800); + } +#endif + } + + CreateBody(newBranch); + + foreach (BallastFloraBranch vine in Branches) + { + vine.UpdateType(); + } + } + + /// + /// Create a body for a branch which works as the hitbox for flamer + /// + /// + private void CreateBody(BallastFloraBranch branch) + { + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } + + Rectangle rect = branch.Rect; + Vector2 pos = Parent.Position + Offset + branch.Position; + + float scale = branch.IsRoot ? 3.0f : 1f; + Body branchBody = GameMain.World.CreateRectangle(ConvertUnits.ToSimUnits(rect.Width * scale), ConvertUnits.ToSimUnits(rect.Height * scale), 1.5f); + branchBody.BodyType = BodyType.Static; + branchBody.UserData = branch; + branchBody.SetCollidesWith(Physics.CollisionRepair); + branchBody.SetCollisionCategories(Physics.CollisionRepair); + branchBody.Position = ConvertUnits.ToSimUnits(pos); + branchBody.Enabled = HasBrokenThrough; + + bodies.Add(branchBody); + } + + public void DamageBranch(BallastFloraBranch branch, float amount, AttackType type, Character? attacker = null) + { + // damage is handled server side currently + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } + + if (attacker != null && toxinsCooldown <= 0) + { + toxinsTimer = 25f; + toxinsCooldown = 60f; + } + + if (type == AttackType.Fire) + { + if (IsInWater(branch)) + { + return; + } + + if (defenseCooldown <= 0) + { + if (!(StateMachine.State is DefendWithPumpState)) + { + StateMachine.EnterState(new DefendWithPumpState(branch, ClaimedTargets, attacker)); + defenseCooldown = 60f; + } + + defenseCooldown = 10f; + } + } + + branch.AccumulatedDamage += amount; + + branch.Health -= amount; + + if (type != AttackType.Other) + { + Anger += amount * 0.001f; + } + +#if SERVER + GameMain.Server?.KarmaManager?.OnBallastFloraDamaged(attacker, amount); +#endif + + if (branch.Health < 0) + { + RemoveBranch(branch); + } + } + + public void Remove() + { + foreach (Body body in bodies) + { + GameMain.World.Remove(body); + } + + Parent.BallastFlora = null; + Branches.Clear(); + + foreach (Item target in ClaimedTargets) + { + target.Infector = null; + } + } + + public void RemoveBranch(BallastFloraBranch branch) + { + bool isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient; + + Anger += 0.01f; + + Branches.Remove(branch); + branch.Removed = true; + + bodies.ForEachMod(body => + { + if (body.UserData == branch) + { + GameMain.World.Remove(body); + bodies.Remove(body); + foreach (var (tileSide, otherBranch) in branch.Connections) + { + TileSide opposite = tileSide.GetOppositeSide(); + otherBranch.BlockedSides &= ~opposite; + otherBranch.Sides &= ~opposite; + + otherBranch.UpdateType(); + + if (isClient) { continue; } + + // Remove branches that are not connected to anything anymore + if ((otherBranch.Type == VineTileType.Stem || otherBranch.Sides == TileSide.None) && !otherBranch.IsRoot) + { + RemoveBranch(otherBranch); + } + } + } + }); + +#if CLIENT + CreateDeathParticle(branch); +#endif + + if (isClient) { return; } + + if (branch.HasClaimedItem) + { + RemoveClaim(branch.ClaimedItem); + } + + if (branch.IsRoot) + { + Kill(); + return; + } + +#if SERVER + SendNetworkMessage(this, NetworkHeader.BranchRemove, branch); +#endif + } + + public void RemoveClaim(ushort id) + { + ClaimedTargets.ForEachMod(item => + { + if (item.ID == id) + { + if (!IgnoredTargets.ContainsKey(item)) + { + IgnoredTargets.Add(item, 10); + } + + ClaimedTargets.Remove(item); + item.Infector = null; + + ClaimedJunctionBoxes.ForEachMod(jb => + { + if (jb.Item == item) + { + ClaimedJunctionBoxes.Remove(jb); + } + }); + + ClaimedBatteries.ForEachMod(bat => + { + if (bat.Item == item) + { + ClaimedBatteries.Remove(bat); + } + }); + +#if SERVER + SendNetworkMessage(this, NetworkHeader.Infect, item.ID, false); +#endif + } + }); + } + + public void Kill() + { + Branches.ForEachMod(RemoveBranch); + Parent.BallastFlora = null; + + foreach (Item target in ClaimedTargets) + { + target.Infector = null; + } + + // clean up leftover (can probably be removed) + foreach (Body body in bodies) + { + Debug.Assert(false, "Leftover bodies found after the ballast flora has died."); + GameMain.World.Remove(body); + } + +#if SERVER + SendNetworkMessage(this, NetworkHeader.Kill); +#endif + } + + private void BreakThrough() + { + HasBrokenThrough = true; + + foreach (Body body in bodies) + { + body.Enabled = true; + } + +#if CLIENT + foreach (BallastFloraBranch branch in Branches) + { + CreateShapnel(GetWorldPosition() + branch.Position); + } + + SoundPlayer.PlayDamageSound(BurstSound, BreakthroughPoint, GetWorldPosition(), range: 800); +#endif + } + + private bool CanGrowMore() => Branches.Any(b => b.CanGrowMore()); + + private bool CollidesWithWorld(Rectangle rect) + { + if (Branches.Any(g => g.Rect.Contains(rect))) { return true; } + + Rectangle worldRect = rect; + worldRect.Location = (Parent.Position + Offset).ToPoint() + worldRect.Location; + worldRect.Y -= worldRect.Height; + + Vector2 topLeft = ConvertUnits.ToSimUnits(new Vector2(worldRect.Left, worldRect.Top)), + topRight = ConvertUnits.ToSimUnits(new Vector2(worldRect.Right, worldRect.Top)), + bottomLeft = ConvertUnits.ToSimUnits(new Vector2(worldRect.Left, worldRect.Bottom)), + bottomRight = ConvertUnits.ToSimUnits(new Vector2(worldRect.Right, worldRect.Bottom)); + + bool hasCollision = LineCollides(topLeft, topRight) || LineCollides(topRight, bottomRight) || LineCollides(bottomRight, bottomLeft) || LineCollides(bottomLeft, topLeft); + + return hasCollision; + } + + private static bool LineCollides(Vector2 point1, Vector2 point2) + { + const Category category = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel; + return Submarine.PickBody(point1, point2, collisionCategory: category, customPredicate: CustomPredicate) != null; + + static bool CustomPredicate(Fixture f) + { + bool hasCollision = f.CollidesWith.HasFlag(Physics.CollisionItem); + Body body = f.Body; + + if (body.UserData == null) { return false; } + + switch (body.UserData) + { + case Submarine _: + case Structure _: + return hasCollision; + default: + return false; + } + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraPrefab.cs new file mode 100644 index 000000000..e053c3104 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraPrefab.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections; +using System.Xml.Linq; + +namespace Barotrauma +{ + class BallastFloraPrefab : IPrefab, IDisposable + { + public string OriginalName { get; } + public string Identifier { get; } + public string FilePath { get; } + public XElement Element { get; } + + public ContentPackage ContentPackage { get; private set; } + + public bool Disposed; + + public static readonly PrefabCollection Prefabs = new PrefabCollection(); + + private BallastFloraPrefab(XElement element, string filePath, bool isOverride) + { + Identifier = element.GetAttributeString("identifier", ""); + OriginalName = element.GetAttributeString("name", ""); + Element = element; + FilePath = filePath; + Prefabs.Add(this, isOverride); + } + + public static BallastFloraPrefab Find(string idenfitier) + { + return !string.IsNullOrWhiteSpace(idenfitier) ? Prefabs.Find(prefab => prefab.Identifier == idenfitier) : null; + } + + public static void LoadAll(IEnumerable files) + { + DebugConsole.Log("Loading map creature prefabs: "); + + foreach (ContentFile file in files) { LoadFromFile(file); } + } + + public static void LoadFromFile(ContentFile file) + { + XDocument doc = XMLExtensions.TryLoadXml(file.Path); + + var rootElement = doc?.Root; + if (rootElement == null) { return; } + + switch (rootElement.Name.ToString().ToLowerInvariant()) + { + case "ballastflorabehavior": + { + new BallastFloraPrefab(rootElement, file.Path, false) { ContentPackage = file.ContentPackage }; + break; + } + case "ballastflorabehaviors": + { + foreach (var element in rootElement.Elements()) + { + if (element.IsOverride()) + { + XElement upgradeElement = element.GetChildElement("mapcreature"); + if (upgradeElement != null) + { + new BallastFloraPrefab(upgradeElement, file.Path, true) { ContentPackage = file.ContentPackage }; + } + else + { + DebugConsole.ThrowError($"Cannot find a map creature element from the children of the override element defined in {file.Path}"); + } + } + else + { + if (element.Name.ToString().Equals("mapcreature", StringComparison.OrdinalIgnoreCase)) + { + new BallastFloraPrefab(element, file.Path, false) { ContentPackage = file.ContentPackage }; + } + } + } + + break; + } + case "override": + { + XElement mapCreatures = rootElement.GetChildElement("ballastflorabehaviors"); + if (mapCreatures != null) + { + foreach (XElement element in mapCreatures.Elements()) + { + new BallastFloraPrefab(element, file.Path, true) { ContentPackage = file.ContentPackage }; + } + } + + foreach (XElement element in rootElement.GetChildElements("ballastflorabehavior")) + { + new BallastFloraPrefab(element, file.Path, true) { ContentPackage = file.ContentPackage }; + } + + break; + } + default: + { + DebugConsole.ThrowError($"Invalid XML root element: '{rootElement.Name}' in {file.Path}\n " + + "Valid elements are: \"MapCreature\", \"MapCreatures\" and \"Override\"."); + break; + } + } + } + + private void Dispose(bool disposing) + { + if (!Disposed) + { + if (disposing) + { + Prefabs.Remove(this); + } + } + + Disposed = true; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/BallastFloraStateMachine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/BallastFloraStateMachine.cs new file mode 100644 index 000000000..7d0eb16bd --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/BallastFloraStateMachine.cs @@ -0,0 +1,49 @@ +#nullable enable +namespace Barotrauma.MapCreatures.Behavior +{ + class BallastFloraStateMachine + { + private readonly BallastFloraBehavior parent; + + public BallastFloraStateMachine(BallastFloraBehavior parent) + { + this.parent = parent; + } + + private IBallastFloraState? lastState; + public IBallastFloraState? State; + + public void EnterState(IBallastFloraState newState) + { + lastState = State; + State?.Exit(); + newState.Enter(); + State = newState; + } + + public void Update(float deltaTime) + { + if (State == null) + { + EnterState(new GrowIdleState(parent)); + return; + } + + State.Update(deltaTime); + + switch (State.GetState()) + { + case ExitState.Running: + break; + + case ExitState.ReturnLast when lastState != null && lastState.GetState() == ExitState.Running: + EnterState(lastState); + break; + + default: + EnterState(new GrowIdleState(parent)); + break; + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/DefendWithPumpState.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/DefendWithPumpState.cs new file mode 100644 index 000000000..ab939cc7e --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/DefendWithPumpState.cs @@ -0,0 +1,126 @@ +using Barotrauma.Items.Components; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma.MapCreatures.Behavior +{ + class DefendWithPumpState : IBallastFloraState + { + private readonly BallastFloraBranch targetBranch; + private readonly List allAvailablePumps = new List(); + private readonly List allAvailableDoors = new List(); + private readonly List targetPumps = new List(); + private readonly List jammedDoors = new List(); + + private bool isFinished; + private float timer = 10f; + private bool filled; + private bool tryDrown; + private readonly Character attacker; + + public DefendWithPumpState(BallastFloraBranch branch, List items, Character attacker) + { + targetBranch = branch; + this.attacker = attacker; + + foreach (Item item in items) + { + if (item.GetComponent() is { } pump) + { + allAvailablePumps.Add(pump); + } + + if (item.GetComponent() is { } door) + { + allAvailableDoors.Add(door); + } + } + } + + public ExitState GetState() + { + if (isFinished) { return ExitState.ReturnLast; } + if (targetBranch.CurrentHull == null) { return ExitState.ReturnLast; } + return timer < 0 ? ExitState.ReturnLast : ExitState.Running; + } + + public void Enter() + { + foreach (Pump pump in allAvailablePumps) + { + if (pump.Item.CurrentHull == targetBranch.CurrentHull) + { + targetPumps.Add(pump); + SetPump(pump); + pump.Hijacked = true; + } + } + + if (!targetPumps.Any() || targetPumps.All(p => !p.HasPower)) + { + isFinished = true; + } + + // lock the doors if the attacker is in the same hull as the ballast flora to try to drown them + if (targetBranch.CurrentHull != null && attacker != null && attacker.CurrentHull == targetBranch.CurrentHull) + { + foreach (Door door in allAvailableDoors) + { + if (door.LinkedGap != null && door.LinkedGap.linkedTo.Contains(targetBranch.CurrentHull)) + { + door.TrySetState(false, false, true); + door.IsJammed = true; + jammedDoors.Add(door); + } + } + + tryDrown = true; + } + } + + public void Exit() + { + foreach (Pump pump in targetPumps) + { + pump.Hijacked = false; + } + + foreach (Door door in jammedDoors) + { + door.IsJammed = false; + } + } + + 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) + { + SetPump(pump); + } + + if (tryDrown && !filled) + { + // keep the ballast filled for extra 10 seconds + if (targetBranch.CurrentHull == null || targetBranch.CurrentHull.WaterPercentage >= 95f) + { + filled = true; + timer += 10f; + } + } + + timer -= deltaTime; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/GrowIdleState.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/GrowIdleState.cs new file mode 100644 index 000000000..69a69b806 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/GrowIdleState.cs @@ -0,0 +1,187 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; +using FarseerPhysics; +using FarseerPhysics.Dynamics; +using Microsoft.Xna.Framework; + +namespace Barotrauma.MapCreatures.Behavior +{ + internal class GrowIdleState: IBallastFloraState + { + public readonly BallastFloraBehavior Behavior; + private float growthTimer; + + public GrowIdleState(BallastFloraBehavior behavior) + { + Behavior = behavior; + } + + public virtual ExitState GetState() => ExitState.Running; + + public virtual void Enter() + { + foreach (BallastFloraBranch branch in Behavior.Branches.Where(b => b.CanGrowMore())) + { + if (TryScanTargets(branch)) { return; } + } + } + + public void Exit() { } + + private bool TryScanTargets(BallastFloraBranch branch) + { + if (ScanForTargets(branch) is { } newTarget) + { + Behavior.StateMachine.EnterState(new GrowToTargetState(Behavior, branch, newTarget)); + return true; + } + + return false; + } + + public void Update(float deltaTime) + { + if (growthTimer > 0) + { + growthTimer -= Behavior.GetGrowthSpeed(deltaTime); + } + else + { + Grow(); + UpdateIgnoredTargets(); + growthTimer = Behavior.GrowthWarps > 0 ? 0f : 5f; + } + } + + protected virtual void Grow() + { + List newTiles = GrowRandomly(); +#if DEBUG || UNSTABLE + Behavior.debugSearchLines.Clear(); +#endif + if (newTiles.Any(TryScanTargets)) { return; } + } + + public void UpdateIgnoredTargets() + { + Behavior.IgnoredTargets.ForEachMod(pair => + { + var (item, delay) = pair; + + if (delay <= 0) + { + Behavior.IgnoredTargets.Remove(item); + } + else + { + Behavior.IgnoredTargets[item] = --delay; + } + }); + } + + private List GrowRandomly() + { + List newBranches = new List(); + List newList = new List(Behavior.Branches); + foreach (BallastFloraBranch branch in newList) + { + if (branch.FailedGrowthAttempts > 8 || !branch.CanGrowMore()) { continue; } + + if (Rand.Range(0, Behavior.Branches.Count(tile => tile.CanGrowMore())) != 0) { continue; } + + TileSide side = branch.GetRandomFreeSide(); + + if (side == TileSide.None) { continue; } + + Behavior.TryGrowBranch(branch, side, out List result); + newBranches.AddRange(result); + } + + return newBranches; + } + + private Item? ScanForTargets(VineTile branch) + { + Hull parent = Behavior.Parent; + Vector2 worldPos = Behavior.GetWorldPosition() + branch.Position; + Vector2 pos = parent.Position + Behavior.Offset + branch.Position; + + Vector2 diameter = ConvertUnits.ToSimUnits(new Vector2(branch.Rect.Width / 2f, branch.Rect.Height / 2f)); + Vector2 topLeft = ConvertUnits.ToSimUnits(pos) - diameter; + Vector2 bottomRight = ConvertUnits.ToSimUnits(pos) + diameter; + + int highestPriority = 0; + Item? currentItem = null; + + foreach (Item item in Item.ItemList.Where(it => !Behavior.ClaimedTargets.Contains(it))) + { + if (Behavior.IgnoredTargets.ContainsKey(item)) { continue; } + + int priority = 0; + foreach (BallastFloraBehavior.AITarget target in Behavior.Targets) + { + if (!target.Matches(item) || target.Priority <= highestPriority) { continue; } + priority = target.Priority; + break; + } + + if (priority == 0) { continue; } + + if (item.Submarine != parent.Submarine || Vector2.Distance(worldPos, item.WorldPosition) > Behavior.Sight) { continue; } + + Vector2 itemSimPos = ConvertUnits.ToSimUnits(item.Position); + +#if DEBUG || UNSTABLE + Tuple debugLine1 = Tuple.Create(parent.Position - ConvertUnits.ToDisplayUnits(topLeft), parent.Position - ConvertUnits.ToDisplayUnits(itemSimPos - diameter)); + Tuple debugLine2 = Tuple.Create(parent.Position - ConvertUnits.ToDisplayUnits(bottomRight), parent.Position - ConvertUnits.ToDisplayUnits(itemSimPos + diameter)); + Behavior.debugSearchLines.Add(debugLine2); + Behavior.debugSearchLines.Add(debugLine1); +#endif + + Body? body1 = Submarine.CheckVisibility(itemSimPos - diameter, topLeft); + if (Blocks(body1, item)) { continue; } + + Body? body2 = Submarine.CheckVisibility(itemSimPos + diameter, bottomRight); + if (Blocks(body2, item)) { continue; } + + highestPriority = priority; + currentItem = item; + } + + if (currentItem != null) + { + foreach (BallastFloraBranch existingBranch in Behavior.Branches) + { + if (Behavior.BranchContainsTarget(existingBranch, currentItem)) + { + Behavior.ClaimTarget(currentItem, existingBranch); + return null; + } + } + + return currentItem; + } + + return null; + + static bool Blocks(Body body, Item target) + { + if (body == null) { return false; } + + switch (body.UserData) + { + case Submarine _: + case Structure _: + case Item it when it != target: + return true; + default: + return false; + } + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/GrowToTargetState.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/GrowToTargetState.cs new file mode 100644 index 000000000..251ad8abb --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/GrowToTargetState.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; + +namespace Barotrauma.MapCreatures.Behavior +{ + class GrowToTargetState: GrowIdleState + { + public readonly List TargetBranches = new List(); + public readonly Item Target; + + private bool isFinished; + + public GrowToTargetState(BallastFloraBehavior behavior, BallastFloraBranch starter, Item target) : base(behavior) + { + Target = target; + TargetBranches.Add(starter); + } + + // do nothing + public override void Enter() { } + + public override ExitState GetState() => isFinished ? ExitState.Terminate : ExitState.Running; + + protected override void Grow() + { + if (Target == null || Target.Removed) + { + isFinished = true; + return; + } + + GrowTowardsTarget(); + } + + private void GrowTowardsTarget() + { + bool succeeded = false; + + List newList = new List(TargetBranches); + foreach (BallastFloraBranch branch in newList) + { + if (branch.FailedGrowthAttempts > 8 || !branch.CanGrowMore()) { continue; } + + // Get what side gets us closest to the target + TileSide side = GetClosestSide(branch, Target.WorldPosition); + + if (branch.IsSideBlocked(side)) { continue; } + + succeeded |= Behavior.TryGrowBranch(branch, side, out List newBranches); + TargetBranches.AddRange(newBranches); + + foreach (BallastFloraBranch newBranch in newBranches) + { + Rectangle worldRect = newBranch.Rect; + worldRect.Location = Behavior.GetWorldPosition().ToPoint() + worldRect.Location; + if (Behavior.BranchContainsTarget(newBranch, Target)) + { + Behavior.ClaimTarget(Target, newBranch); + isFinished = true; + return; + } + } + } + + if (!succeeded) + { + if (!Behavior.IgnoredTargets.ContainsKey(Target)) + { + Behavior.IgnoredTargets.Add(Target, 1); + } + + isFinished = true; + } + } + + private TileSide GetClosestSide(VineTile tile, Vector2 targetPos) + { + var (distX, distY) = tile.Position + Behavior.GetWorldPosition() - targetPos; + int absDistX = (int) Math.Abs(distX), absDistY = (int) Math.Abs(distY); + + return absDistX > absDistY ? distX > 0 ? TileSide.Left : TileSide.Right : distY > 0 ? TileSide.Bottom : TileSide.Top; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/IBallastFloraState.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/IBallastFloraState.cs new file mode 100644 index 000000000..9261de598 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/IBallastFloraState.cs @@ -0,0 +1,21 @@ +#if CLIENT +using Microsoft.Xna.Framework.Graphics; +#endif + +namespace Barotrauma.MapCreatures.Behavior +{ + enum ExitState + { + Running, // State is running + Terminate, // State has exited + ReturnLast // Return to the last running state if any + } + + interface IBallastFloraState + { + public void Enter(); + public void Exit(); + public void Update(float deltaTime); + public ExitState GetState(); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs index 0ab0abaf8..b08738e22 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs @@ -11,6 +11,9 @@ namespace Barotrauma { public const ushort NullEntityID = 0; public const ushort EntitySpawnerID = ushort.MaxValue; + public const ushort RespawnManagerID = ushort.MaxValue - 1; + + public const ushort ReservedIDStart = ushort.MaxValue - 2; private static Dictionary dictionary = new Dictionary(); public static IEnumerable GetEntities() @@ -22,8 +25,6 @@ namespace Barotrauma public static EntitySpawner Spawner; - private ushort id; - protected AITarget aiTarget; private bool idFreed; @@ -39,53 +40,7 @@ namespace Barotrauma get { return idFreed; } } - public ushort ID - { - get - { - return id; - } - set - { - if (this is EntitySpawner) { return; } - if (value == NullEntityID) - { - DebugConsole.ThrowError("Cannot set the ID of an entity to " + NullEntityID + - "! The value is reserved for entity events referring to a non-existent (e.g. removed) entity.\n" + Environment.StackTrace.CleanupStackTrace()); - return; - } - if (value == EntitySpawnerID) - { - DebugConsole.ThrowError("Cannot set the ID of an entity to " + EntitySpawnerID + - "! The value is reserved for EntitySpawner.\n" + Environment.StackTrace.CleanupStackTrace()); - return; - } - - if (dictionary.TryGetValue(id, out Entity thisEntity) && thisEntity == this) - { - dictionary.Remove(id); - } - //if there's already an entity with the same ID, give it the old ID of this one - if (dictionary.TryGetValue(value, out Entity existingEntity)) - { - DebugConsole.Log(existingEntity + " had the same ID as " + this + " (" + value + ")"); - dictionary.Remove(value); - dictionary.Add(id, existingEntity); - existingEntity.id = id; - DebugConsole.Log("The id of " + existingEntity + " is now " + id); - DebugConsole.Log("The id of " + this + " is now " + value); - } - - id = value; - idFreed = false; - dictionary.Add(id, this); - } - } - - /// - /// The ID the entity had after instantiation/loading. May have been taken up by another entity, causing a new ID to be assigned to this entity. - /// - public ushort OriginalID; + public readonly ushort ID; public virtual Vector2 SimPosition { @@ -125,17 +80,27 @@ namespace Barotrauma private readonly double spawnTime; - public Entity(Submarine submarine) + public Entity(Submarine submarine, ushort id) { this.Submarine = submarine; spawnTime = Timing.TotalTime; + if (id != NullEntityID && dictionary.ContainsKey(id)) + { + throw new Exception($"ID {id} is taken by {dictionary[id].ToString()}"); + } + //give a unique ID - id = OriginalID = this is EntitySpawner ? - EntitySpawnerID : - FindFreeID(submarine == null ? (ushort)1 : submarine.IdOffset); + ID = DetermineID(id, submarine); - dictionary.Add(id, this); + dictionary.Add(ID, this); + } + + protected virtual ushort DetermineID(ushort id, Submarine submarine) + { + return id != NullEntityID ? + id : + FindFreeID(submarine == null ? (ushort)1 : submarine.IdOffset); } public static ushort FindFreeID(ushort idOffset = 0) @@ -153,7 +118,7 @@ namespace Barotrauma { id += 1; IDfound = dictionary.ContainsKey(id); - } while (IDfound || id == NullEntityID || id == EntitySpawnerID); + } while (IDfound || id == NullEntityID || id > ReservedIDStart); return id; } @@ -192,7 +157,7 @@ namespace Barotrauma errorMsg.AppendLine("Some entities were not removed in Entity.RemoveAll:"); foreach (Entity e in dictionary.Values) { - errorMsg.AppendLine(" - " + e.ToString() + "(ID " + e.id + ")"); + errorMsg.AppendLine(" - " + e.ToString() + "(ID " + e.ID + ")"); } } if (Item.ItemList.Count > 0) @@ -200,7 +165,7 @@ namespace Barotrauma errorMsg.AppendLine("Some items were not removed in Entity.RemoveAll:"); foreach (Item item in Item.ItemList) { - errorMsg.AppendLine(" - " + item.Name + "(ID " + item.id + ")"); + errorMsg.AppendLine(" - " + item.Name + "(ID " + item.ID + ")"); } var items = new List(Item.ItemList); @@ -222,7 +187,7 @@ namespace Barotrauma errorMsg.AppendLine("Some characters were not removed in Entity.RemoveAll:"); foreach (Character character in Character.CharacterList) { - errorMsg.AppendLine(" - " + character.Name + "(ID " + character.id + ")"); + errorMsg.AppendLine(" - " + character.Name + "(ID " + character.ID + ")"); } var characters = new List(Character.CharacterList); @@ -293,15 +258,15 @@ namespace Barotrauma public static void DumpIds(int count, string filename) { - List entities = dictionary.Values.OrderByDescending(e => e.id).ToList(); + List entities = dictionary.Values.OrderByDescending(e => e.ID).ToList(); count = Math.Min(entities.Count, count); List lines = new List(); for (int i = 0; i < count; i++) { - lines.Add(entities[i].id + ": " + entities[i].ToString()); - DebugConsole.ThrowError(entities[i].id + ": " + entities[i].ToString()); + lines.Add(entities[i].ID + ": " + entities[i].ToString()); + DebugConsole.ThrowError(entities[i].ID + ": " + entities[i].ToString()); } if (!string.IsNullOrWhiteSpace(filename)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index 439ccdcdf..10ae9497f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -6,6 +6,8 @@ using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; +using Barotrauma.Extensions; +using Barotrauma.MapCreatures.Behavior; namespace Barotrauma { @@ -30,8 +32,10 @@ namespace Barotrauma private readonly float decalSize; public float EmpStrength { get; set; } + + 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) { @@ -39,6 +43,7 @@ namespace Barotrauma }; this.force = force; this.EmpStrength = empStrength; + BallastFloraDamage = ballastFloraStrength; sparks = true; shockwave = true; smoke = true; @@ -65,6 +70,7 @@ namespace Barotrauma if (element.Attribute("flashrange") != null) { flashRange = element.GetAttributeFloat("flashrange", 100.0f); } EmpStrength = element.GetAttributeFloat("empstrength", 0.0f); + BallastFloraDamage = element.GetAttributeFloat("ballastfloradamage", 0.0f); decal = element.GetAttributeString("decal", ""); decalSize = element.GetAttributeFloat(1.0f, "decalSize", "decalsize"); @@ -125,7 +131,12 @@ namespace Barotrauma 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) + { + RangedBallastFloraDamage(worldPosition, displayRange, BallastFloraDamage, attacker); } if (EmpStrength > 0.0f) @@ -167,8 +178,12 @@ namespace Barotrauma foreach (Item item in Item.ItemList) { if (item.Condition <= 0.0f) { continue; } - if (Vector2.Distance(item.WorldPosition, worldPosition) > attack.Range * 0.5f) { continue; } - if (applyFireEffects && !item.FireProof) + 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 * 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) @@ -177,9 +192,9 @@ namespace Barotrauma while (container != null) { if (container.FireProof) - { - fireProof = true; - break; + { + fireProof = true; + break; } container = container.Container; } @@ -190,20 +205,18 @@ namespace Barotrauma { GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnFire }); } - } + } } if (item.Prefab.DamagedByExplosions && !item.Indestructible) { - float limbRadius = item.body == null ? 0.0f : item.body.GetMaxExtent(); - float dist = Vector2.Distance(item.WorldPosition, worldPosition); - dist = Math.Max(0.0f, dist - ConvertUnits.ToDisplayUnits(limbRadius)); - if (dist > attack.Range) - { - continue; - } 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; } + + damageAmount *= GetObstacleDamageMultiplier(ConvertUnits.ToSimUnits(explosionPos), worldPosition, item.SimPosition); item.Condition -= damageAmount * distFactor; } } @@ -241,7 +254,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); @@ -255,37 +268,7 @@ namespace Barotrauma float distFactor = 1.0f - dist / attack.Range; //solid obstacles between the explosion and the limb reduce the effect of the explosion - var obstacles = Submarine.PickBodies(limb.SimPosition, explosionPos, collisionCategory: Physics.CollisionItem | Physics.CollisionItemBlocking | Physics.CollisionWall); - foreach (var body in obstacles) - { - if (body.UserData is Item item) - { - var door = item.GetComponent(); - if (door != null && !door.IsBroken) { distFactor *= 0.01f; } - } - else if (body.UserData is Structure structure) - { - int sectionIndex = structure.FindSectionIndex(worldPosition, world: true, clamp: true); - if (structure.SectionBodyDisabled(sectionIndex)) - { - continue; - } - else if (structure.SectionIsLeaking(sectionIndex)) - { - distFactor *= 0.1f; - } - else - { - distFactor *= 0.01f; - } - } - else - { - distFactor *= 0.1f; - } - } - if (distFactor <= 0.05f) { continue; } - + distFactor *= GetObstacleDamageMultiplier(explosionPos, worldPosition, limb.SimPosition); distFactors.Add(limb, distFactor); modifiedAfflictions.Clear(); @@ -372,7 +355,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) + public static Dictionary RangedStructureDamage(Vector2 worldPosition, float worldRange, float damage, float levelWallDamage, Character attacker = null) { List structureList = new List(); float dist = 600.0f; @@ -395,23 +378,123 @@ namespace Barotrauma { float distFactor = 1.0f - (Vector2.Distance(structure.SectionPosition(i, true), worldPosition) / worldRange); if (distFactor <= 0.0f) continue; - + structure.AddDamage(i, damage * distFactor, attacker); if (damagedStructures.ContainsKey(structure)) - { + { damagedStructures[structure] += damage * distFactor; } else { damagedStructures.Add(structure, damage * distFactor); } - } + } + } + + if (Level.Loaded != null && !MathUtils.NearlyEqual(levelWallDamage, 0.0f)) + { + for (int i = Level.Loaded.ExtraWalls.Count - 1; i >= 0; i--) + { + if (!(Level.Loaded.ExtraWalls[i] is DestructibleLevelWall destructibleWall)) { continue; } + foreach (var cell in destructibleWall.Cells) + { + if (cell.IsPointInside(worldPosition)) + { + destructibleWall.AddDamage(levelWallDamage, worldPosition); + continue; + } + foreach (var edge in cell.Edges) + { + if (!MathUtils.GetLineIntersection(worldPosition, cell.Center, edge.Point1 + cell.Translation, edge.Point2 + cell.Translation, out Vector2 intersection)) + { + continue; + } + + float wallDist = Vector2.DistanceSquared(worldPosition, intersection); + if (wallDist < worldRange * worldRange) + { + destructibleWall.AddDamage(damage, worldPosition); + break; + } + } + } + } } return damagedStructures; } + public void RangedBallastFloraDamage(Vector2 worldPosition, float worldRange, float damage, Character attacker = null) + { + List ballastFlorae = new List(); + + foreach (Hull hull in Hull.hullList) + { + if (hull.BallastFlora != null) { ballastFlorae.Add(hull.BallastFlora); } + } + + foreach (BallastFloraBehavior ballastFlora in ballastFlorae) + { + float resistanceMuliplier = ballastFlora.HasBrokenThrough ? 1f : 1f - ballastFlora.ExplosionResistance; + ballastFlora.Branches.ForEachMod(branch => + { + Vector2 branchWorldPos = ballastFlora.GetWorldPosition() + branch.Position; + float branchDist = Vector2.Distance(branchWorldPos, worldPosition); + if (branchDist < worldRange) + { + float distFactor = 1.0f - (branchDist / worldRange); + if (distFactor <= 0.0f) { return; } + + Vector2 explosionPos = worldPosition; + Vector2 branchPos = branchWorldPos; + if (ballastFlora.Parent?.Submarine != null) + { + explosionPos -= ballastFlora.Parent.Submarine.Position; + branchPos -= ballastFlora.Parent.Submarine.Position; + } + distFactor *= GetObstacleDamageMultiplier(ConvertUnits.ToSimUnits(explosionPos), worldPosition, ConvertUnits.ToSimUnits(branchPos)); + ballastFlora.DamageBranch(branch, damage * distFactor * resistanceMuliplier, BallastFloraBehavior.AttackType.Explosives, attacker); + } + }); + } + } + + private static float GetObstacleDamageMultiplier(Vector2 explosionSimPos, Vector2 explosionWorldPos, Vector2 targetSimPos) + { + float damageMultiplier = 1.0f; + var obstacles = Submarine.PickBodies(targetSimPos, explosionSimPos, collisionCategory: Physics.CollisionItem | Physics.CollisionItemBlocking | Physics.CollisionWall); + foreach (var body in obstacles) + { + if (body.UserData is Item item) + { + var door = item.GetComponent(); + if (door != null && !door.IsBroken) { damageMultiplier *= 0.01f; } + } + else if (body.UserData is Structure structure) + { + int sectionIndex = structure.FindSectionIndex(explosionWorldPos, world: true, clamp: true); + if (structure.SectionBodyDisabled(sectionIndex)) + { + continue; + } + else if (structure.SectionIsLeaking(sectionIndex)) + { + damageMultiplier *= 0.1f; + } + else + { + damageMultiplier *= 0.01f; + } + } + else + { + damageMultiplier *= 0.1f; + } + } + return damageMultiplier; + } + static partial void PlayTinnitusProjSpecific(float volume); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs index 7da6b79b7..f8a8b23a3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs @@ -302,7 +302,7 @@ namespace Barotrauma return; } - float dmg = (float)Math.Sqrt(Math.Min(500, size.X)) * deltaTime / c.AnimController.Limbs.Count(l => !l.IsSevered); + float dmg = (float)Math.Sqrt(Math.Min(500, size.X)) * deltaTime / c.AnimController.Limbs.Count(l => !l.IsSevered && !l.Hidden); foreach (Limb limb in c.AnimController.Limbs) { if (limb.IsSevered) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs index daba696ec..950041ad6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs @@ -129,8 +129,8 @@ namespace Barotrauma : this(rect, rect.Width < rect.Height, submarine) { } - public Gap(Rectangle rect, bool isHorizontal, Submarine submarine) - : base(MapEntityPrefab.Find(null, "gap"), submarine) + public Gap(Rectangle rect, bool isHorizontal, Submarine submarine, ushort id = Entity.NullEntityID) + : base(MapEntityPrefab.Find(null, "gap"), submarine, id) { this.rect = rect; flowForce = Vector2.Zero; @@ -141,7 +141,8 @@ namespace Barotrauma GapList.Add(this); InsertToList(); - outsideCollisionBlocker = GameMain.World.CreateEdge(-Vector2.UnitX * 2.0f, Vector2.UnitX * 2.0f); + float blockerSize = ConvertUnits.ToSimUnits(Math.Max(rect.Width, rect.Height)) / 2; + outsideCollisionBlocker = GameMain.World.CreateEdge(-Vector2.UnitX * blockerSize, Vector2.UnitX * blockerSize); outsideCollisionBlocker.UserData = $"CollisionBlocker (Gap {ID})"; outsideCollisionBlocker.BodyType = BodyType.Static; outsideCollisionBlocker.CollisionCategories = Physics.CollisionWall; @@ -711,7 +712,7 @@ namespace Barotrauma if (!DisableHullRechecks) FindHulls(); } - public static Gap Load(XElement element, Submarine submarine) + public static Gap Load(XElement element, Submarine submarine, IdRemap idRemap) { Rectangle rect = Rectangle.Empty; @@ -737,12 +738,10 @@ namespace Barotrauma isHorizontal = horizontalAttribute.Value.ToString() == "true"; } - Gap g = new Gap(rect, isHorizontal, submarine) + Gap g = new Gap(rect, isHorizontal, submarine, idRemap.GetOffsetId(element)) { - ID = (ushort)int.Parse(element.Attribute("ID").Value), linkedToID = new List(), }; - g.OriginalID = g.ID; return g; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index 2bfd020c4..c069066ae 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Xml.Linq; +using Barotrauma.MapCreatures.Behavior; namespace Barotrauma { @@ -391,14 +392,16 @@ namespace Barotrauma public List FakeFireSources { get; private set; } + public BallastFloraBehavior BallastFlora { get; set; } + public Hull(MapEntityPrefab prefab, Rectangle rectangle) : this (prefab, rectangle, Submarine.MainSub) { } - public Hull(MapEntityPrefab prefab, Rectangle rectangle, Submarine submarine) - : base (prefab, submarine) + public Hull(MapEntityPrefab prefab, Rectangle rectangle, Submarine submarine, ushort id = Entity.NullEntityID) + : base (prefab, submarine, id) { rect = rectangle; @@ -524,6 +527,8 @@ namespace Barotrauma } } Pressure = rect.Y - rect.Height + waterVolume / rect.Width; + + BallastFlora?.OnMapLoaded(); } public void AddToGrid(Submarine submarine) @@ -607,6 +612,7 @@ namespace Barotrauma { base.Remove(); hullList.Remove(this); + BallastFlora?.Remove(); if (Submarine != null && !Submarine.Loading && !Submarine.Unloading) { @@ -687,6 +693,9 @@ namespace Barotrauma public override void Update(float deltaTime, Camera cam) { base.Update(deltaTime, cam); + + BallastFlora?.Update(deltaTime); + UpdateProjSpecific(deltaTime, cam); Oxygen -= OxygenDeteriorationSpeed * deltaTime; @@ -1352,7 +1361,7 @@ namespace Barotrauma } #endregion - public static Hull Load(XElement element, Submarine submarine) + public static Hull Load(XElement element, Submarine submarine, IdRemap idRemap) { Rectangle rect; if (element.Attribute("rect") != null) @@ -1369,23 +1378,13 @@ namespace Barotrauma int.Parse(element.Attribute("height").Value)); } - var hull = new Hull(MapEntityPrefab.Find(null, "hull"), rect, submarine) + var hull = new Hull(MapEntityPrefab.Find(null, "hull"), rect, submarine, idRemap.GetOffsetId(element)) { - WaterVolume = element.GetAttributeFloat("pressure", 0.0f), - ID = (ushort)int.Parse(element.Attribute("ID").Value) + WaterVolume = element.GetAttributeFloat("pressure", 0.0f) }; - hull.OriginalID = hull.ID; hull.linkedToID = new List(); - string linkedToString = element.GetAttributeString("linked", ""); - if (linkedToString != "") - { - string[] linkedToIds = linkedToString.Split(','); - for (int i = 0; i < linkedToIds.Length; i++) - { - hull.linkedToID.Add((ushort)int.Parse(linkedToIds[i])); - } - } + hull.ParseLinks(element, idRemap); string originalAmbientLight = element.GetAttributeString("originalambientlight", null); if (!string.IsNullOrWhiteSpace(originalAmbientLight)) @@ -1410,6 +1409,15 @@ namespace Barotrauma decal.BaseAlpha = baseAlpha; } break; + case "ballastflorabehavior": + string identifier = subElement.GetAttributeString("identifier", string.Empty); + BallastFloraPrefab prefab = BallastFloraPrefab.Find(identifier); + if (prefab != null) + { + hull.BallastFlora = new BallastFloraBehavior(hull, prefab, Vector2.Zero); + hull.BallastFlora.LoadSave(subElement); + } + break; } } @@ -1488,6 +1496,8 @@ namespace Barotrauma )); } + BallastFlora?.Save(element); + SerializableProperty.SerializeProperties(this, element); parentElement.Add(element); return element; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/ISpatialEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/ISpatialEntity.cs index cbc2c193e..88d8bb92f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/ISpatialEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/ISpatialEntity.cs @@ -8,5 +8,6 @@ namespace Barotrauma Vector2 WorldPosition { get; } Vector2 SimPosition { get; } Submarine Submarine { get; } + bool IgnoreByAI => false; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs index aee63270b..8037cdbe6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs @@ -114,14 +114,16 @@ namespace Barotrauma #if CLIENT if (Screen.Selected is SubEditorScreen) { - SubEditorScreen.StoreCommand(new AddOrDeleteCommand(loaded, false)); + SubEditorScreen.StoreCommand(new AddOrDeleteCommand(loaded, false, handleInventoryBehavior: false)); } #endif } public List CreateInstance(Vector2 position, Submarine sub, bool selectPrefabs = false) { - List entities = MapEntity.LoadAll(sub, configElement, FilePath); + int idOffset = Entity.FindFreeID(1); + if (MapEntity.mapEntityList.Any()) { idOffset = MapEntity.mapEntityList.Max(e => e.ID); } + List entities = MapEntity.LoadAll(sub, configElement, FilePath, idOffset); if (entities.Count == 0) { return entities; } Vector2 offset = sub == null ? Vector2.Zero : sub.HiddenSubPosition; @@ -132,7 +134,16 @@ namespace Barotrauma me.Submarine = sub; if (!(me is Item item)) { continue; } Wire wire = item.GetComponent(); - if (wire != null) { wire.MoveNodes(position - offset); } + //Vector2 subPosition = Submarine == null ? Vector2.Zero : Submarine.HiddenSubPosition; + if (wire != null) + { + //fix wires that have been erroneously saved at the "hidden position" + if (sub != null && Vector2.Distance(me.Position, sub.HiddenSubPosition) > sub.HiddenSubPosition.Length() / 2) + { + me.Move(position); + } + wire.MoveNodes(position - offset); + } } MapEntity.MapLoaded(entities, true); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerationParams.cs new file mode 100644 index 000000000..2bf8a1b1b --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerationParams.cs @@ -0,0 +1,197 @@ +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + class CaveGenerationParams : ISerializableEntity + { + public static List CaveParams { get; private set; } + + public string Name + { + get { return Identifier; } + } + + public readonly string Identifier; + + private int minWidth, maxWidth; + private int minHeight, maxHeight; + + private int minBranchCount, maxBranchCount; + + public Dictionary SerializableProperties + { + get; + set; + } + + /// + /// Overrides the commonness of the object in a specific level type. + /// Key = name of the level type, value = commonness in that level type. + /// + public readonly Dictionary OverrideCommonness = new Dictionary(); + + [Editable, Serialize(1.0f, true)] + public float Commonness + { + get; + private set; + } + + [Serialize(8000, true), Editable(MinValueInt = 1000, MaxValueInt = 100000)] + public int MinWidth + { + get { return minWidth; } + set { minWidth = Math.Max(value, 1000); } + } + + [Serialize(10000, true), Editable(MinValueInt = 1000, MaxValueInt = 1000000)] + public int MaxWidth + { + get { return maxWidth; } + set { maxWidth = Math.Max(value, minWidth); } + } + + [Serialize(8000, true), Editable(MinValueInt = 1000, MaxValueInt = 100000)] + public int MinHeight + { + get { return minHeight; } + set { minHeight = Math.Max(value, 1000); } + } + + [Serialize(10000, true), Editable(MinValueInt = 1000, MaxValueInt = 1000000)] + public int MaxHeight + { + get { return maxHeight; } + set { maxHeight = Math.Max(value, minHeight); } + } + + [Serialize(2, true), Editable(MinValueInt = 0, MaxValueInt = 10)] + public int MinBranchCount + { + get { return minBranchCount; } + set { minBranchCount = Math.Max(value, 0); } + } + + [Serialize(4, true), Editable(MinValueInt = 0, MaxValueInt = 10)] + public int MaxBranchCount + { + get { return maxBranchCount; } + set { maxBranchCount = Math.Max(value, minBranchCount); } + } + + [Serialize(50, true), Editable(MinValueInt = 0, MaxValueInt = 1000)] + public int LevelObjectAmount + { + get; + set; + } + + [Serialize(0.1f, true), Editable(MinValueFloat = 0, MaxValueFloat = 1.0f, DecimalCount = 2 )] + public float DestructibleWallRatio + { + get; + set; + } + + public Sprite WallSprite { get; private set; } + public Sprite WallEdgeSprite { get; private set; } + + public static CaveGenerationParams GetRandom(LevelGenerationParams generationParams, Rand.RandSync rand) + { + if (CaveParams.All(p => p.GetCommonness(generationParams) <= 0.0f)) + { + return CaveParams.First(); + } + return ToolBox.SelectWeightedRandom(CaveParams, CaveParams.Select(p => p.GetCommonness(generationParams)).ToList(), rand); + } + + public float GetCommonness(LevelGenerationParams generationParams) + { + if (generationParams?.Identifier != null && + OverrideCommonness.TryGetValue(generationParams.Identifier, out float commonness)) + { + return commonness; + } + return Commonness; + } + + private CaveGenerationParams(XElement element) + { + Identifier = element == null ? "default" : element.GetAttributeString("identifier", null) ?? element.Name.ToString(); + Identifier = Identifier.ToLowerInvariant(); + SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + + foreach (XElement subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "wall": + WallSprite = new Sprite(subElement); + break; + case "walledge": + WallEdgeSprite = new Sprite(subElement); + break; + } + } + } + + public static void LoadPresets() + { + CaveParams = new List(); + + var files = GameMain.Instance.GetFilesOfType(ContentType.CaveGenerationParameters); + if (!files.Any()) + { + files = new List() { new ContentFile("Content/Map/CaveGenerationParameters.xml", ContentType.CaveGenerationParameters) }; + } + + foreach (ContentFile file in files) + { + XDocument doc = XMLExtensions.TryLoadXml(file.Path); + if (doc == null) { continue; } + var mainElement = doc.Root; + if (doc.Root.IsOverride()) + { + mainElement = doc.Root.FirstElement(); + CaveParams.Clear(); + DebugConsole.NewMessage($"Overriding cave generation parameters with '{file.Path}'", Color.Yellow); + } + + foreach (XElement element in mainElement.Elements()) + { + bool isOverride = element.IsOverride(); + if (isOverride) + { + string identifier = element.FirstElement().GetAttributeString("identifier", ""); + var existingParams = CaveParams.Find(p => p.Identifier.Equals(identifier, StringComparison.OrdinalIgnoreCase)); + if (existingParams != null) + { + DebugConsole.NewMessage($"Overriding the cave generation parameters '{identifier}' using the file '{file.Path}'", Color.Yellow); + CaveParams.Remove(existingParams); + } + CaveParams.Add(new CaveGenerationParams(element.FirstElement())); + + } + else + { + string identifier = element.FirstElement().GetAttributeString("identifier", ""); + var existingParams = CaveParams.Find(p => p.Identifier.Equals(identifier, StringComparison.OrdinalIgnoreCase)); + if (existingParams != null) + { + DebugConsole.ThrowError($"Duplicate cave generation parameters: '{identifier}' defined in {element.Name} of '{file.Path}'. Use tags to override the generation parameters."); + continue; + } + else + { + CaveParams.Add(new CaveGenerationParams(element)); + } + } + } + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs index b2403d1f7..03ce1a9b4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs @@ -1,4 +1,5 @@ using FarseerPhysics; +using FarseerPhysics.Collision.Shapes; using FarseerPhysics.Common; using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; @@ -80,7 +81,7 @@ namespace Barotrauma } } if (MathUtils.NearlyEqual(ge.Point2.X, borders.X) || MathUtils.NearlyEqual(ge.Point2.X, borders.Right) || - MathUtils.NearlyEqual(ge.Point2.Y, borders.Y) || MathUtils.NearlyEqual(ge.Point2.Y, borders.Bottom)) + MathUtils.NearlyEqual(ge.Point2.Y, borders.Y) || MathUtils.NearlyEqual(ge.Point2.Y, borders.Bottom)) { if (point1 == null) { @@ -95,14 +96,43 @@ namespace Barotrauma if (point1.HasValue && point2.HasValue) { Debug.Assert(point1 != point2); - var newEdge = new GraphEdge(point1.Value, point2.Value) + bool point1OnSide = MathUtils.NearlyEqual(point1.Value.X, borders.X) || MathUtils.NearlyEqual(point1.Value.X, borders.Right); + bool point2OnSide = MathUtils.NearlyEqual(point2.Value.X, borders.X) || MathUtils.NearlyEqual(point2.Value.X, borders.Right); + //one point is one the side, another on top/bottom + // -> the cell is in the corner of the level, we need 2 edges + if (point1OnSide != point2OnSide) { - Cell1 = cell, - IsSolid = true, - Site1 = cell.Site, - OutsideLevel = true - }; - cell.Edges.Add(newEdge); + Vector2 cornerPos = new Vector2( + point1.Value.X < borders.Center.X ? borders.X : borders.Right, + point1.Value.Y < borders.Center.Y ? borders.Y : borders.Bottom); + cell.Edges.Add( + new GraphEdge(point1.Value, cornerPos) + { + Cell1 = cell, + IsSolid = true, + Site1 = cell.Site, + OutsideLevel = true + }); + cell.Edges.Add( + new GraphEdge(point2.Value, cornerPos) + { + Cell1 = cell, + IsSolid = true, + Site1 = cell.Site, + OutsideLevel = true + }); + } + else + { + cell.Edges.Add( + new GraphEdge(point1.Value, point2.Value) + { + Cell1 = cell, + IsSolid = true, + Site1 = cell.Site, + OutsideLevel = true + }); + } break; } } @@ -111,44 +141,16 @@ 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 List GeneratePath( - List pathNodes, List cells, List[,] cellGrid, - int gridCellSize, Rectangle limits, float wanderAmount = 0.3f, bool mirror = false) + public static void GeneratePath(Level.Tunnel tunnel, List cells, List[,] cellGrid, int gridCellSize, Rectangle limits) { var targetCells = new List(); - for (int i = 0; i < pathNodes.Count; i++) + 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) { - int cellIndex = FindCellIndex(pathNodes[i], cells, cellGrid, gridCellSize, searchDepth); + int cellIndex = FindCellIndex(tunnel.Nodes[i], cells, cellGrid, gridCellSize, searchDepth); if (cellIndex > -1) { targetCells.Add(cells[cellIndex]); @@ -157,25 +159,15 @@ namespace Barotrauma searchDepth++; } - } - - return GeneratePath(targetCells, cells, cellGrid, gridCellSize, limits, wanderAmount, mirror); + tunnel.Cells.AddRange(GeneratePath(targetCells, cells, limits)); } - - public static List GeneratePath( - List targetCells, List cells, List[,] cellGrid, - int gridCellSize, Rectangle limits, float wanderAmount = 0.3f, bool mirror = false) + public static List GeneratePath(List targetCells, List cells, Rectangle limits) { Stopwatch sw2 = new Stopwatch(); sw2.Start(); - //how heavily the path "steers" towards the endpoint - //lower values will cause the path to "wander" more, higher will make it head straight to the end - wanderAmount = MathHelper.Clamp(wanderAmount, 0.0f, 1.0f); - - List allowedEdges = new List(); List pathCells = new List(); VoronoiCell currentCell = targetCells[0]; @@ -190,41 +182,24 @@ namespace Barotrauma { int edgeIndex = 0; - allowedEdges.Clear(); - foreach (GraphEdge edge in currentCell.Edges) + double smallestDist = double.PositiveInfinity; + for (int i = 0; i < currentCell.Edges.Count; i++) { - var adjacentCell = edge.AdjacentCell(currentCell); - if (adjacentCell != null && limits.Contains(adjacentCell.Site.Coord.X, adjacentCell.Site.Coord.Y)) + var adjacentCell = currentCell.Edges[i].AdjacentCell(currentCell); + 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) { - allowedEdges.Add(edge); + dist += 1000000; } - } - - //steer towards target - if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) > wanderAmount || allowedEdges.Count == 0) - { - double smallestDist = double.PositiveInfinity; - for (int i = 0; i < currentCell.Edges.Count; i++) + if (dist < smallestDist) { - var adjacentCell = currentCell.Edges[i].AdjacentCell(currentCell); - 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); - if (dist < smallestDist) - { - edgeIndex = i; - smallestDist = dist; - } + edgeIndex = i; + smallestDist = dist; } } - //choose random edge (ignoring ones where the adjacent cell is outside limits) - else - { - edgeIndex = Rand.Int(allowedEdges.Count, Rand.RandSync.Server); - if (mirror && edgeIndex > 0) edgeIndex = allowedEdges.Count - edgeIndex; - edgeIndex = currentCell.Edges.IndexOf(allowedEdges[edgeIndex]); - } currentCell = currentCell.Edges[edgeIndex].AdjacentCell(currentCell); currentCell.CellType = CellType.Path; @@ -234,8 +209,8 @@ namespace Barotrauma if (currentCell == targetCells[currentTargetIndex]) { - currentTargetIndex += 1; - if (currentTargetIndex >= targetCells.Count) break; + currentTargetIndex++; + if (currentTargetIndex >= targetCells.Count) { break; } } } while (currentCell != targetCells[targetCells.Count - 1] && iterationsLeft > 0); @@ -256,17 +231,42 @@ namespace Barotrauma List tempEdges = new List(); foreach (GraphEdge edge in cell.Edges) { - if (!edge.IsSolid) + if (!edge.IsSolid || edge.OutsideLevel) { tempEdges.Add(edge); continue; } + //If the edge is next to an empty cell and there's another solid cell at the other side of the empty one, + //don't touch this edge. Otherwise we may end up closing off small passages between cells. + var adjacentEmptyCell = edge.AdjacentCell(cell); + if (adjacentEmptyCell?.CellType == CellType.Solid) { adjacentEmptyCell = null; } + if (adjacentEmptyCell != null) + { + GraphEdge adjacentEdge = null; + //find the edge at the opposite side of the adjacent cell + foreach (GraphEdge otherEdge in adjacentEmptyCell.Edges) + { + if (Vector2.Dot(adjacentEmptyCell.Center - edge.Center, adjacentEmptyCell.Center - otherEdge.Center) < 0 && + otherEdge.AdjacentCell(adjacentEmptyCell)?.CellType == CellType.Solid) + { + adjacentEdge = otherEdge; + break; + } + } + if (adjacentEdge != null) + { + tempEdges.Add(edge); + continue; + } + } + 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); + Vector2 edgeDir = edge.Point2 - edge.Point1; for (int i = 0; i <= pointCount; i++) { if (i == 0) @@ -279,16 +279,48 @@ namespace Barotrauma } else { + float centerF = 0.5f - Math.Abs(0.5f - (i / (float)pointCount)); float randomVariance = Rand.Range(0, irregularity, Rand.RandSync.Server); - edgePoints.Add( + Vector2 extrudedPoint = edge.Point1 + - edgeDir * (i / (float)pointCount) - - edgeNormal * edgeLength * (roundingAmount + randomVariance) * centerF); + edgeDir * (i / (float)pointCount) + + edgeNormal * edgeLength * (roundingAmount + randomVariance) * centerF; + + 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); + } } } - for (int i = 0; i < pointCount; i++) + for (int i = 0; i < edgePoints.Count - 1; i++) { tempEdges.Add(new GraphEdge(edgePoints[i], edgePoints[i + 1]) { @@ -297,7 +329,10 @@ namespace Barotrauma IsSolid = edge.IsSolid, Site1 = edge.Site1, Site2 = edge.Site2, - OutsideLevel = edge.OutsideLevel + OutsideLevel = edge.OutsideLevel, + NextToCave = edge.NextToCave, + NextToMainPath = edge.NextToMainPath, + NextToSidePath = edge.NextToSidePath }); } } @@ -347,15 +382,27 @@ namespace Barotrauma continue; } - renderTriangles.AddRange(MathUtils.TriangulateConvexHull(tempVertices, cell.Center)); - - if (bodyPoints.Count < 2) continue; + 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; } if (bodyPoints.Count < 3) { foreach (Vector2 vertex in tempVertices) { - if (bodyPoints.Contains(vertex)) continue; + if (bodyPoints.Contains(vertex)) { continue; } bodyPoints.Add(vertex); break; } @@ -366,11 +413,11 @@ namespace Barotrauma cell.BodyVertices.Add(bodyPoints[i]); bodyPoints[i] = ConvertUnits.ToSimUnits(bodyPoints[i]); } - - if (cell.CellType == CellType.Empty) continue; + + 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++) { @@ -380,13 +427,17 @@ namespace Barotrauma Vector2 b = triangles[i][1]; Vector2 c = triangles[i][2]; float area = Math.Abs(a.X * (b.Y - c.Y) + b.X * (c.Y - a.Y) + c.X * (a.Y - b.Y)) / 2.0f; - if (area < 1.0f) continue; + if (area < 1.0f) { continue; } Vertices bodyVertices = new Vertices(triangles[i]); - var newFixture = cellBody.CreatePolygon(bodyVertices, 5.0f); - newFixture.UserData = cell; + PolygonShape polygon = new PolygonShape(bodyVertices, 5.0f); + Fixture fixture = new Fixture(polygon) + { + UserData = cell + }; + cellBody.Add(fixture, resetMassData: false); - if (newFixture.Shape.MassData.Area < FarseerPhysics.Settings.Epsilon) + if (fixture.Shape.MassData.Area < FarseerPhysics.Settings.Epsilon) { DebugConsole.ThrowError("Invalid triangle created by CaveGenerator (" + triangles[i][0] + ", " + triangles[i][1] + ", " + triangles[i][2] + ")"); GameAnalyticsManager.AddErrorEventOnce( @@ -394,11 +445,13 @@ namespace Barotrauma GameAnalyticsSDK.Net.EGAErrorSeverity.Warning, "Invalid triangle created by CaveGenerator (" + triangles[i][0] + ", " + triangles[i][1] + ", " + triangles[i][2] + "). Seed: " + level.Seed); } - } - + } cell.Body = cellBody; } + cellBody.CollisionCategories = Physics.CollisionLevel; + cellBody.ResetMassData(); + return cellBody; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/DestructibleLevelWall.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/DestructibleLevelWall.cs new file mode 100644 index 000000000..8c648d559 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/DestructibleLevelWall.cs @@ -0,0 +1,214 @@ +using FarseerPhysics; +using FarseerPhysics.Dynamics; +using Microsoft.Xna.Framework; +using System.Collections.Generic; +using Voronoi2; + +namespace Barotrauma +{ + partial class DestructibleLevelWall : LevelWall, IDamageable + { + public bool NetworkUpdatePending; + + public float Damage + { + get; + private set; + } + + public float MaxHealth + { + get; + private set; + } = 1000.0f; + + public bool Destroyed + { + get; + private set; + } + + public float FadeOutDuration + { + get; + private set; + } + + public float FadeOutTimer + { + get; + private set; + } + + public Vector2 SimPosition + { + get { return Body.Position; } + } + + public Vector2 WorldPosition + { + get { return ConvertUnits.ToDisplayUnits(Body.Position); } + } + + public float Health + { + get { return MaxHealth - Damage; } + } + + 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); + Cells.ForEach(c => c.IsDestructible = true); + } + + public override void Update(float deltaTime) + { + base.Update(deltaTime); + if (FadeOutDuration > 0.0f) + { + FadeOutTimer += deltaTime; + if (FadeOutTimer > FadeOutDuration && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsClient)) { Destroy(); } + } + } + + public void AddDamage(float damage, Vector2 worldPosition) + { + AddDamageProjSpecific(damage, worldPosition); + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } + if (Destroyed) { return; } + if (!MathUtils.NearlyEqual(damage, 0.0f)) { NetworkUpdatePending = true; } + Damage += damage; + if (Damage >= MaxHealth) + { + CreateFragments(); + Destroy(); + } + } + + partial void AddDamageProjSpecific(float damage, Vector2 worldPosition); + + + public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = true) + { + AddDamage(attack.StructureDamage, worldPosition); + return new AttackResult(attack.StructureDamage); + } + + private void CreateFragments() + { +#if CLIENT + SoundPlayer.PlaySound("icebreak", WorldPosition); +#endif + //generate initial triangles (one triangle from each edge to the center of the cell) + List> triangles = new List>(); + foreach (var cell in Cells) + { + foreach (GraphEdge edge in cell.Edges) + { + List triangleVerts = new List + { + edge.Point1 + cell.Translation, + edge.Point2 + cell.Translation, + cell.Center + }; + triangles.Add(triangleVerts); + } + } + + //split triangles that have edges more than 1000 units long + Pair longestEdge = new Pair(-1, -1); + float longestEdgeLength = 0.0f; + do + { + longestEdge.First = -1; + longestEdge.Second = -1; + longestEdgeLength = 0.0f; + for (int i = 0; i < triangles.Count; i++) + { + for (int edge = 0; edge < 3; edge++) + { + float edgeLength = Vector2.Distance(triangles[i][edge], triangles[i][(edge + 1) % 3]); + if (edgeLength > longestEdgeLength) + { + longestEdge.First = i; + longestEdge.Second = edge; + longestEdgeLength = edgeLength; + } + } + } + if (longestEdgeLength < 1000.0f) + { + break; + } + Vector2 p0 = triangles[longestEdge.First][longestEdge.Second]; + Vector2 p1 = triangles[longestEdge.First][(longestEdge.Second + 1) % 3]; + Vector2 p2 = triangles[longestEdge.First][(longestEdge.Second + 2) % 3]; + triangles[longestEdge.First] = new List { p0, (p0 + p1) / 2, p2 }; + triangles.Add(new List { (p0 + p1) / 2, p1, p2 }); + + + } while (triangles.Count < 32); + + //generate fragments + foreach (var triangle in triangles) + { + Vector2 triangleCenter = (triangle[0] + triangle[1]+ triangle[2]) / 3; + triangle[0] -= triangleCenter; + triangle[1] -= triangleCenter; + triangle[2] -= triangleCenter; + Vector2 simTriangleCenter = ConvertUnits.ToSimUnits(triangleCenter); + + DestructibleLevelWall fragment = new DestructibleLevelWall(triangle, Color.White, Level.Loaded, giftWrap: true); + fragment.Damage = fragment.MaxHealth; + fragment.Body.Position = simTriangleCenter; + fragment.Body.BodyType = BodyType.Dynamic; + fragment.Body.FixedRotation = false; + fragment.Body.LinearDamping = Rand.Range(0.2f, 0.3f); + fragment.Body.AngularDamping = Rand.Range(0.1f, 0.2f); + fragment.Body.GravityScale = 0.1f; + fragment.Body.Mass *= 10.0f; + fragment.Body.CollisionCategories = Physics.CollisionNone; + fragment.Body.CollidesWith = Physics.CollisionWall; + fragment.FadeOutDuration = 20.0f; + + Vector2 bodyDiff = simTriangleCenter - Body.Position; + fragment.Body.LinearVelocity = (bodyDiff + Rand.Vector(0.5f)).ClampLength(15.0f); + fragment.Body.AngularVelocity = Rand.Range(-0.5f, 0.5f);// MathHelper.Clamp(-bodyDiff.X * 0.1f, -0.5f, 0.5f); + + Level.Loaded.UnsyncedExtraWalls.Add(fragment); + +#if CLIENT + for (int i = 0; i < 20; i++) + { + int startEdgeIndex = Rand.Int(3); + Vector2 pos1 = triangle[startEdgeIndex]; + Vector2 pos2 = triangle[(startEdgeIndex + 1) % 3]; + + var particle = GameMain.ParticleManager.CreateParticle("iceshards", + triangleCenter + Vector2.Lerp(pos1, pos2, Rand.Range(0.0f, 1.0f)), + Rand.Vector(Rand.Range(50.0f, 1000.0f)) + fragment.Body.LinearVelocity * 100.0f); + if (particle != null) + { + particle.Size *= Rand.Range(1.0f, 5.0f); + } + } +#endif + } + } + + public void Destroy() + { + 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 a714f13dd..24c843ff0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -30,7 +30,12 @@ namespace Barotrauma [Flags] public enum PositionType { - MainPath = 1, Cave = 2, Ruin = 4, Wreck = 8 + MainPath = 0x1, + SidePath = 0x2, + Cave = 0x4, + Ruin = 0x8, + Wreck = 0x10, + BeaconStation = 0x20 } public struct InterestingPosition @@ -51,12 +56,79 @@ namespace Barotrauma } } + public enum TunnelType + { + MainPath, SidePath, Cave + } + + public class Tunnel + { + public readonly Tunnel ParentTunnel; + + public readonly int MinWidth; + + public readonly TunnelType Type; + + public List Nodes + { + get; + private set; + } + + public List Cells + { + get; + private set; + } + + public List WayPoints + { + get; + private set; + } + + public Tunnel(TunnelType type, List nodes, int minWidth, Tunnel parentTunnel) + { + Type = type; + MinWidth = minWidth; + ParentTunnel = parentTunnel; + Nodes = new List(nodes); + Cells = new List(); + WayPoints = new List(); + } + } + + public class Cave + { + public Rectangle Area; + + public readonly List Tunnels = new List(); + + public Point StartPos, EndPos; + + public readonly CaveGenerationParams CaveGenerationParams; + + public Cave(CaveGenerationParams caveGenerationParams, Rectangle area, Point startPos, Point endPos) + { + CaveGenerationParams = caveGenerationParams; + Area = area; + StartPos = startPos; + EndPos = endPos; + } + } + //how close the sub has to be to start/endposition to exit public const float ExitDistance = 6000.0f; public const int GridCellSize = 2000; private List[,] cellGrid; private List cells; - + + //TODO: make private + public List siteCoordsX, siteCoordsY; + + //TODO: make private + public List> distanceField; + private Point startPosition, endPosition; private readonly Rectangle borders; @@ -97,15 +169,40 @@ namespace Barotrauma private set; } + public const float DefaultRealWorldCrushDepth = 3500.0f; + + public float CrushDepth + { + get + { + return LevelData.CrushDepth; + } + } + + public float RealWorldCrushDepth + { + get + { + return LevelData.RealWorldCrushDepth; + } + } + public LevelWall SeaFloor { get; private set; } public List Ruins { get; private set; } public List Wrecks { get; private set; } + public Submarine BeaconStation { get; private set; } + private Sonar beaconSonar; + public List ExtraWalls { get; private set; } - public List> SmallTunnels { get; private set; } = new List>(); + public List UnsyncedExtraWalls { get; private set; } + + public List Tunnels { get; private set; } = new List(); + + public List Caves { get; private set; } = new List(); public List PositionsOfInterest { get; private set; } @@ -196,13 +293,13 @@ namespace Barotrauma get { return LevelData.GenerationParams.WallColor; } } - private Level(LevelData levelData) : base(null) + private Level(LevelData levelData) : base(null, 0) { this.LevelData = levelData; borders = new Rectangle(Point.Zero, levelData.Size); //remove from entity dictionary - base.Remove(); + //base.Remove(); } public static Level Generate(LevelData levelData, bool mirror, SubmarineInfo startOutpost = null, SubmarineInfo endOutpost = null) @@ -256,6 +353,7 @@ namespace Barotrauma PositionsOfInterest = new List(); ExtraWalls = new List(); + UnsyncedExtraWalls = new List(); bodies = new List(); List sites = new List(); @@ -299,43 +397,44 @@ namespace Barotrauma //generate the initial nodes for the main path and smaller tunnels //---------------------------------------------------------------------------------- - List pathNodes = new List { startPosition }; + Tunnel mainPath = new Tunnel( + TunnelType.MainPath, + GeneratePathNodes(startPosition, endPosition, pathBorders, null, GenerationParams.MainPathVariance), + minWidth, parentTunnel: null); + Tunnels.Add(mainPath); - Point nodeInterval = GenerationParams.MainPathNodeIntervalRange; - - for (int x = startPosition.X + nodeInterval.X; - x < endPosition.X - nodeInterval.X; - x += Rand.Range(nodeInterval.X, nodeInterval.Y, Rand.RandSync.Server)) + int sideTunnelCount = Rand.Range(GenerationParams.SideTunnelCount.X, GenerationParams.SideTunnelCount.Y + 1, Rand.RandSync.Server); + for (int j = 0; j < sideTunnelCount; j++) { - pathNodes.Add(new Point(x, Rand.Range(pathBorders.Y, pathBorders.Bottom, Rand.RandSync.Server))); + if (mainPath.Nodes.Count < 4) { break; } + var validTunnels = Tunnels.FindAll(t => t.Type != TunnelType.Cave); + Tunnel tunnelToBranchOff = validTunnels[Rand.Int(validTunnels.Count, Rand.RandSync.Server)]; + if (tunnelToBranchOff == null) { tunnelToBranchOff = mainPath; } + + Point branchStart = tunnelToBranchOff.Nodes[Rand.Range(0, tunnelToBranchOff.Nodes.Count / 3, Rand.RandSync.Server)]; + Point branchEnd = tunnelToBranchOff.Nodes[Rand.Range(tunnelToBranchOff.Nodes.Count / 3 * 2, tunnelToBranchOff.Nodes.Count - 1, Rand.RandSync.Server)]; + + var sidePathNodes = GeneratePathNodes(branchStart, branchEnd, pathBorders, tunnelToBranchOff, GenerationParams.SideTunnelVariance); + //make sure the path is wide enough to pass through + int pathWidth = Rand.Range(GenerationParams.MinSideTunnelRadius.X, GenerationParams.MinSideTunnelRadius.Y, Rand.RandSync.Server); + Tunnels.Add(new Tunnel(TunnelType.SidePath, sidePathNodes, pathWidth, parentTunnel: tunnelToBranchOff)); } - if (pathNodes.Count == 1) - { - pathNodes.Add(new Point(pathBorders.Center.X, pathBorders.Y)); - } - //if all nodes ended up high up in the level, move one down to make sure we utilize the full height of the level - else if (pathNodes.GetRange(1, pathNodes.Count - 1).All(p => p.Y > pathBorders.Y + pathBorders.Height * 0.25f)) - { - int nodeIndex = Rand.Range(1, pathNodes.Count, Rand.RandSync.Server); - pathNodes[nodeIndex] = new Point(pathNodes[nodeIndex].X, pathBorders.Y); - } - - pathNodes.Add(endPosition); - - GenerateTunnels(pathNodes, minWidth); + CalculateTunnelDistanceField(density: 1000); + GenerateSeaFloorPositions(mirror); + GenerateCaves(mainPath); EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); //---------------------------------------------------------------------------------- //generate voronoi sites //---------------------------------------------------------------------------------- - + Point siteInterval = GenerationParams.VoronoiSiteInterval; int siteIntervalSqr = (siteInterval.X * siteInterval.X + siteInterval.Y * siteInterval.Y); Point siteVariance = GenerationParams.VoronoiSiteVariance; - List siteCoordsX = new List((borders.Height / siteInterval.Y) * (borders.Width / siteInterval.Y)); - List siteCoordsY = new List((borders.Height / siteInterval.Y) * (borders.Width / siteInterval.Y)); + siteCoordsX = new List((borders.Height / siteInterval.Y) * (borders.Width / siteInterval.Y)); + siteCoordsY = new List((borders.Height / siteInterval.Y) * (borders.Width / siteInterval.Y)); for (int x = siteInterval.X / 2; x < borders.Width; x += siteInterval.X) { for (int y = siteInterval.Y / 2; y < borders.Height; y += siteInterval.Y) @@ -343,23 +442,55 @@ namespace Barotrauma int siteX = x + Rand.Range(-siteVariance.X, siteVariance.X, Rand.RandSync.Server); int siteY = y + Rand.Range(-siteVariance.Y, siteVariance.Y, Rand.RandSync.Server); - if (SmallTunnels.Any(t => t.Any(node => MathUtils.DistanceSquared(node.X, node.Y, siteX, siteY) < siteIntervalSqr))) + bool closeToTunnel = false; + bool closeToCave = false; + foreach (Tunnel tunnel in Tunnels) { - //add some more sites around the small tunnels to generate more small voronoi cells + for (int i = 1; i < tunnel.Nodes.Count; i++) + { + float minDist = Math.Max(tunnel.MinWidth, Math.Max(siteInterval.X, siteInterval.Y)) * 2.0f; + 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; } + if (siteY > Math.Max(tunnel.Nodes[i - 1].Y, tunnel.Nodes[i].Y) + minDist) { continue; } + + double tunnelDistSqr = MathUtils.LineSegmentToPointDistanceSquared(tunnel.Nodes[i - 1], tunnel.Nodes[i], new Point(siteX, siteY)); + if (Math.Sqrt(tunnelDistSqr) < minDist) + { + closeToTunnel = true; + tunnelDistSqr = MathUtils.LineSegmentToPointDistanceSquared(tunnel.Nodes[i - 1], tunnel.Nodes[i], new Point(siteX, siteY)); + if (tunnel.Type == TunnelType.Cave ) + { + closeToCave = true; + } + break; + } + } + } + + if (!closeToTunnel) + { + //make the graph less dense (90% less nodes) in areas far away from tunnels where we don't need a lot of geometry + 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); + siteCoordsX.Add(x + Rand.Range(siteInterval.X / 4, siteInterval.X / 2, Rand.RandSync.Server)); siteCoordsY.Add(y); } if (y < borders.Height - siteInterval.Y) { siteCoordsX.Add(x); - siteCoordsY.Add(y + siteInterval.Y / 2); + siteCoordsY.Add(y + Rand.Range(siteInterval.Y / 4, siteInterval.Y / 2, Rand.RandSync.Server)); } if (x < borders.Width - siteInterval.X && y < borders.Height - siteInterval.Y) { - siteCoordsX.Add(x + siteInterval.X / 2); - siteCoordsY.Add(y + siteInterval.Y / 2); + siteCoordsX.Add(x + Rand.Range(siteInterval.X / 4, siteInterval.X / 2, Rand.RandSync.Server)); + siteCoordsY.Add(y + Rand.Range(siteInterval.Y / 4, siteInterval.Y / 2, Rand.RandSync.Server)); } } @@ -388,25 +519,78 @@ namespace Barotrauma Debug.WriteLine("find cells: " + sw2.ElapsedMilliseconds + " ms"); sw2.Restart(); - + //---------------------------------------------------------------------------------- - // generate a path through the initial path nodes + // generate a path through the tunnel nodes //---------------------------------------------------------------------------------- - List mainPath = CaveGenerator.GeneratePath(pathNodes, cells, cellGrid, GridCellSize, - new Rectangle(pathBorders.X, pathBorders.Y, pathBorders.Width, borders.Height), 0.5f, false); - - for (int i = 2; i < mainPath.Count; i += 3) + List pathCells = new List(); + foreach (Tunnel tunnel in Tunnels) { - PositionsOfInterest.Add(new InterestingPosition( - new Point((int)mainPath[i].Site.Coord.X, (int)mainPath[i].Site.Coord.Y), - PositionType.MainPath)); + CaveGenerator.GeneratePath(tunnel, cells, cellGrid, GridCellSize, pathBorders); + if (tunnel.Type == TunnelType.MainPath || tunnel.Type == TunnelType.SidePath) + { + for (int i = 2; i < tunnel.Cells.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)); + } + } + GenerateWaypoints(tunnel, parentTunnel: tunnel.ParentTunnel); + EnlargePath(tunnel.Cells, tunnel.MinWidth); + foreach (var pathCell in tunnel.Cells) + { + MarkEdges(pathCell, tunnel.Type); + foreach (GraphEdge edge in pathCell.Edges) + { + var adjacent = edge.AdjacentCell(pathCell); + if (adjacent != null) + { + MarkEdges(adjacent, tunnel.Type); + } + } + if (!pathCells.Contains(pathCell)) + { + pathCells.Add(pathCell); + } + } + + static void MarkEdges(VoronoiCell cell, TunnelType tunnelType) + { + foreach (GraphEdge edge in cell.Edges) + { + switch (tunnelType) + { + case TunnelType.MainPath: + edge.NextToMainPath = true; + break; + case TunnelType.SidePath: + edge.NextToSidePath = true; + break; + case TunnelType.Cave: + edge.NextToCave = true; + break; + } + } + } } - List pathCells = new List(mainPath); - - //make sure the path is wide enough to pass through - EnlargeMainPath(pathCells, minWidth); + var potentialIslands = new List(); + foreach (var cell in pathCells) + { + if (GetDistToTunnel(cell.Center, mainPath) < minWidth) { continue; } + if (cell.Edges.Any(e => e.AdjacentCell(cell)?.CellType != CellType.Path || e.NextToCave)) { continue; } + potentialIslands.Add(cell); + } + for (int i = 0; i < GenerationParams.IslandCount; i++) + { + if (potentialIslands.Count == 0) { break; } + var island = potentialIslands.GetRandom(Rand.RandSync.Server); + island.CellType = CellType.Solid; + island.Island = true; + pathCells.Remove(island); + } foreach (InterestingPosition positionOfInterest in PositionsOfInterest) { @@ -420,55 +604,23 @@ namespace Barotrauma EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); - //---------------------------------------------------------------------------------- - // tunnels through the tunnel nodes - //---------------------------------------------------------------------------------- - - List> validTunnels = new List>(); - foreach (List tunnel in SmallTunnels) - { - if (tunnel.Count < 2) continue; - - //find the cell which the path starts from - int startCellIndex = CaveGenerator.FindCellIndex(tunnel[0], cells, cellGrid, GridCellSize, 1); - if (startCellIndex < 0) continue; - - //if it wasn't one of the cells in the main path, don't create a tunnel - if (cells[startCellIndex].CellType != CellType.Path) continue; - - int mainPathCellCount = 0; - for (int j = 0; j < tunnel.Count; j++) - { - int tunnelCellIndex = CaveGenerator.FindCellIndex(tunnel[j], cells, cellGrid, GridCellSize, 1); - if (tunnelCellIndex > -1 && cells[tunnelCellIndex].CellType == CellType.Path) mainPathCellCount++; - } - if (mainPathCellCount > tunnel.Count / 2) continue; - - var newPathCells = CaveGenerator.GeneratePath(tunnel, cells, cellGrid, GridCellSize, pathBorders); - if (newPathCells.Any()) - { - PositionsOfInterest.Add(new InterestingPosition(newPathCells.Last().Center.ToPoint(), PositionType.Cave)); - if (newPathCells.Count > 4) { PositionsOfInterest.Add(new InterestingPosition(newPathCells[newPathCells.Count / 2].Center.ToPoint(), PositionType.Cave)); } - } - validTunnels.Add(tunnel); - pathCells.AddRange(newPathCells); - } - SmallTunnels = validTunnels; - - sw2.Restart(); - - //---------------------------------------------------------------------------------- // remove unnecessary cells and create some holes at the bottom of the level //---------------------------------------------------------------------------------- - - cells = CleanCells(pathCells); + + 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.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; - int yPadding = borders.Height / 5; - pathCells.AddRange(CreateHoles(GenerationParams.BottomHoleProbability, new Rectangle( - xPadding, 0, - borders.Width - xPadding * 2, borders.Height - yPadding), minWidth)); + pathCells.AddRange(CreateHoles(GenerationParams.BottomHoleProbability, new Rectangle(xPadding, 0, borders.Width - xPadding * 2, Size.Y / 2), minWidth)); foreach (VoronoiCell cell in cells) { @@ -532,12 +684,18 @@ namespace Barotrauma } } - - foreach (List smallTunnel in SmallTunnels) + foreach (Cave cave in Caves) { - for (int i = 0; i < smallTunnel.Count; i++) + cave.Area = new Rectangle(borders.Width - cave.Area.Right, cave.Area.Y, cave.Area.Width, cave.Area.Height); + cave.StartPos = new Point(borders.Width - cave.StartPos.X, cave.StartPos.Y); + cave.EndPos = new Point(borders.Width - cave.EndPos.X, cave.EndPos.Y); + } + + foreach (Tunnel tunnel in Tunnels) + { + for (int i = 0; i < tunnel.Nodes.Count; i++) { - smallTunnel[i] = new Point(borders.Width - smallTunnel[i].X, smallTunnel[i].Y); + tunnel.Nodes[i] = new Point(borders.Width - tunnel.Nodes[i].X, tunnel.Nodes[i].Y); } } @@ -561,13 +719,33 @@ namespace Barotrauma 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); } + foreach (Cave cave in Caves) + { + 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) + { + var chunk = CreateIceChunk(caveCell.Edges, caveCell.Center, health: 50.0f); + if (chunk != null) + { + chunk.Body.BodyType = BodyType.Static; + ExtraWalls.Add(chunk); + } + } + } + } + EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); //---------------------------------------------------------------------------------- @@ -577,7 +755,7 @@ namespace Barotrauma Ruins = new List(); for (int i = 0; i < GenerationParams.RuinCount; i++) { - GenerateRuin(mainPath, mirror); + GenerateRuin(mainPath.Cells, mirror); } EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); @@ -591,29 +769,23 @@ namespace Barotrauma List iceChunkPositions = new List(); foreach (InterestingPosition pos in PositionsOfInterest) { - if (pos.PositionType != PositionType.MainPath || pos.Position.X < 5000 || pos.Position.X > Size.X - 5000) continue; - if (Math.Abs(pos.Position.X - StartPosition.X) < minWidth * 2 || Math.Abs(pos.Position.X - EndPosition.X) < minWidth * 2) continue; - if (GetTooCloseCells(pos.Position.ToVector2(), minWidth * 0.7f).Count > 0) continue; + if (pos.PositionType != PositionType.MainPath && pos.PositionType != PositionType.SidePath) { continue; } + if (pos.Position.X < 5000 || pos.Position.X > Size.X - 5000) { continue; } + if (Math.Abs(pos.Position.X - StartPosition.X) < minWidth * 2 || Math.Abs(pos.Position.X - EndPosition.X) < minWidth * 2) { continue; } + if (GetTooCloseCells(pos.Position.ToVector2(), minWidth * 0.7f).Count > 0) { continue; } iceChunkPositions.Add(pos.Position); } for (int i = 0; i < GenerationParams.FloatingIceChunkCount; i++) { - if (iceChunkPositions.Count == 0) break; + if (iceChunkPositions.Count == 0) { break; } Point selectedPos = iceChunkPositions[Rand.Int(iceChunkPositions.Count, Rand.RandSync.Server)]; float chunkRadius = Rand.Range(500.0f, 1000.0f, Rand.RandSync.Server); - var newChunk = new LevelWall(CaveGenerator.CreateRandomChunk(chunkRadius, 8, chunkRadius * 0.8f), Color.White, this, true) - { - MoveSpeed = Rand.Range(100.0f, 200.0f, Rand.RandSync.Server), - MoveAmount = new Vector2(0.0f, minWidth * 0.7f) - }; - newChunk.Body.Position = ConvertUnits.ToSimUnits(selectedPos.ToVector2()); - newChunk.Body.BodyType = BodyType.Dynamic; - newChunk.Body.FixedRotation = true; - newChunk.Body.LinearDamping = 0.5f; - newChunk.Body.IgnoreGravity = true; - newChunk.Body.Mass *= 10.0f; - ExtraWalls.Add(newChunk); + var vertices = CaveGenerator.CreateRandomChunk(chunkRadius, 8, chunkRadius * 0.8f); + var chunk = CreateIceChunk(vertices, selectedPos.ToVector2()); + chunk.MoveAmount = new Vector2(0.0f, minWidth * 0.7f); + chunk.MoveSpeed = Rand.Range(100.0f, 200.0f, Rand.RandSync.Server); + ExtraWalls.Add(chunk); iceChunkPositions.Remove(selectedPos); } } @@ -629,7 +801,7 @@ namespace Barotrauma foreach (GraphEdge ge in cell.Edges) { VoronoiCell adjacentCell = ge.AdjacentCell(cell); - ge.IsSolid = (adjacentCell == null || !cells.Contains(adjacentCell)); + ge.IsSolid = adjacentCell == null || !cells.Contains(adjacentCell); } } @@ -645,15 +817,102 @@ namespace Barotrauma } } - bodies.Add(CaveGenerator.GeneratePolygons(cellsWithBody, this, out List triangles)); #if CLIENT - renderer.SetBodyVertices(CaveGenerator.GenerateRenderVerticeList(triangles).ToArray(), GenerationParams.WallColor); - renderer.SetWallVertices(CaveGenerator.GenerateWallShapes(cellsWithBody, this), GenerationParams.WallColor); + List, Cave>> cellBatches = new List, Cave>> + { + new Pair, Cave>(cellsWithBody.ToList(), null) + }; + foreach (Cave cave in Caves) + { + cellBatches.Add(new Pair, Cave>(new List(), cave)); + foreach (var caveCell in cave.Tunnels.SelectMany(t => t.Cells)) + { + foreach (var edge in caveCell.Edges) + { + if (!edge.NextToCave) { continue; } + if (edge.Cell1?.CellType == CellType.Solid && !cellBatches.Last().First.Contains(edge.Cell1)) + { + cellBatches.First().First.Remove(edge.Cell1); + cellBatches.Last().First.Add(edge.Cell1); + } + if (edge.Cell2?.CellType == CellType.Solid && !cellBatches.Last().First.Contains(edge.Cell2)) + { + cellBatches.First().First.Remove(edge.Cell2); + cellBatches.Last().First.Add(edge.Cell2); + } + } + } + } + + Debug.Assert(cellsWithBody.Count == cellBatches.Sum(cb => cb.First.Count)); + + List> triangleLists = new List>(); + foreach (Pair, Cave> cellBatch in cellBatches) + { + bodies.Add(CaveGenerator.GeneratePolygons(cellBatch.First, this, out List triangles)); + triangleLists.Add(triangles); + } +#else + bodies.Add(CaveGenerator.GeneratePolygons(cellsWithBody, this, out List triangles)); +#endif + foreach (VoronoiCell cell in cells) + { + CompareCCW compare = new CompareCCW(cell.Center); + foreach (GraphEdge edge in cell.Edges) + { + //remove references to cells that we failed to generate a body for + if (edge.Cell1 != null && edge.Cell1.Body == null && edge.Cell1.CellType != CellType.Empty) { edge.Cell1 = null; } + if (edge.Cell2 != null && edge.Cell2.Body == null && edge.Cell2.CellType != CellType.Empty) { edge.Cell2 = null; } + + //make the order of the points CCW + if (compare.Compare(edge.Point1, edge.Point2) == -1) + { + var temp = edge.Point1; + edge.Point1 = edge.Point2; + edge.Point2 = temp; + } + } + } + +#if CLIENT + Debug.Assert(triangleLists.Count == cellBatches.Count); + for (int i = 0; i < triangleLists.Count; i++) + { + 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, + GenerationParams.WallColor); + } #endif + EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); + //---------------------------------------------------------------------------------- + // create ice spires + //---------------------------------------------------------------------------------- + + List usedSpireEdges = new List(); + for (int i = 0; i < GenerationParams.IceSpireCount; i++) + { + var spire = CreateIceSpire(usedSpireEdges); + if (spire != null) { ExtraWalls.Add(spire); }; + } + + //---------------------------------------------------------------------------------- + // connect side paths and cave branches to their parents + //---------------------------------------------------------------------------------- + + foreach (Tunnel tunnel in Tunnels) + { + if (tunnel.ParentTunnel == null) { continue; } + if (tunnel.Type == TunnelType.Cave && tunnel.ParentTunnel == mainPath) { continue; } + ConnectWaypoints(tunnel, tunnel.ParentTunnel); + } + //---------------------------------------------------------------------------------- // create outposts at the start and end of the level //---------------------------------------------------------------------------------- @@ -676,7 +935,7 @@ namespace Barotrauma bodies.Add(TopBarrier); - GenerateSeaFloor(mirror); + GenerateSeaFloor(); if (mirror) { @@ -694,21 +953,28 @@ namespace Barotrauma } CreateWrecks(); + CreateBeaconStation(cells); + + EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); + LevelObjectManager.PlaceObjects(this, GenerationParams.LevelObjectAmount); + + EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); + GenerateItems(); EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); #if CLIENT - backgroundCreatureManager.SpawnSprites(80); + backgroundCreatureManager.SpawnCreatures(this, GenerationParams.BackgroundCreatureAmount); #endif foreach (VoronoiCell cell in cells) { foreach (GraphEdge edge in cell.Edges) { - edge.Cell1 = null; - edge.Cell2 = null; + //edge.Cell1 = null; + //edge.Cell2 = null; edge.Site1 = null; edge.Site2 = null; } @@ -741,10 +1007,78 @@ namespace Barotrauma #endif //assign an ID to make entity events work - ID = FindFreeID(); + //ID = FindFreeID(); Generating = false; } + private List GeneratePathNodes(Point startPosition, Point endPosition, Rectangle pathBorders, Tunnel parentTunnel, float variance) + { + List pathNodes = new List { startPosition }; + + Point nodeInterval = GenerationParams.MainPathNodeIntervalRange; + + for (int x = startPosition.X + nodeInterval.X; + x < endPosition.X - nodeInterval.X; + x += Rand.Range(nodeInterval.X, nodeInterval.Y, Rand.RandSync.Server)) + { + Point nodePos = new Point(x, Rand.Range(pathBorders.Y, pathBorders.Bottom, Rand.RandSync.Server)); + + //allow placing the 2nd main path node at any height regardless of variance + //(otherwise low variance will always make the main path go through the upper part of the level) + if (pathNodes.Count > 2 || parentTunnel != null) + { + nodePos.Y = (int)MathHelper.Clamp( + nodePos.Y, + pathNodes.Last().Y - pathBorders.Height * variance * 0.5f, + pathNodes.Last().Y + pathBorders.Height * variance * 0.5f); + } + if (pathNodes.Count == 1) + { + //if the path starts below the center of the level, head up and vice versa + //to utilize as much of the vertical space as possible + nodePos.Y = (int)(startPosition.Y + Math.Abs(nodePos.Y - startPosition.Y) * -Math.Sign(nodePos.Y - pathBorders.Center.Y)); + nodePos.Y = MathHelper.Clamp(nodePos.Y, pathBorders.Y, pathBorders.Bottom); + } + + //prevent intersections with other tunnels + foreach (Tunnel tunnel in Tunnels) + { + for (int i = 1; i < tunnel.Nodes.Count; i++) + { + Point node1 = tunnel.Nodes[i - 1]; + Point node2 = tunnel.Nodes[i]; + if (node1.X >= nodePos.X) { continue; } + if (node2.X <= pathNodes.Last().X) { continue; } + if (MathUtils.NearlyEqual(node1.X, pathNodes.Last().X)) { continue; } + if (Math.Abs(node1.Y - nodePos.Y) > tunnel.MinWidth && Math.Abs(node2.Y - nodePos.Y) > tunnel.MinWidth && + !MathUtils.LinesIntersect(node1.ToVector2(), node2.ToVector2(), pathNodes.Last().ToVector2(), nodePos.ToVector2())) + { + continue; + } + + if (nodePos.Y < pathNodes.Last().Y) + { + nodePos.Y = Math.Min(Math.Max(node1.Y, node2.Y) + tunnel.MinWidth * 2, pathBorders.Bottom); + } + else + { + nodePos.Y = Math.Max(Math.Min(node1.Y, node2.Y) - tunnel.MinWidth * 2, pathBorders.Y); + } + break; + } + } + + pathNodes.Add(nodePos); + } + + if (pathNodes.Count == 1) + { + pathNodes.Add(new Point(pathBorders.Center.X, pathBorders.Y)); + } + + pathNodes.Add(endPosition); + return pathNodes; + } private List CreateHoles(float holeProbability, Rectangle limits, int submarineSize) { @@ -763,25 +1097,26 @@ namespace Barotrauma } } + if (cell.Edges.Any(e => e.NextToCave)) { continue; } if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) > holeProbability) { continue; } - if (!limits.Contains(cell.Site.Coord.X, cell.Site.Coord.Y)) { continue; } float closestDist = 0.0f; - WayPoint closestWayPoint = null; - foreach (WayPoint wp in WayPoint.WayPointList) + Point? closestTunnelNode = null; + foreach (Tunnel tunnel in Tunnels) { - if (wp.SpawnType != SpawnType.Path){ continue; } - - float dist = Math.Abs(cell.Center.X - wp.WorldPosition.X); - if (closestWayPoint == null || dist < closestDist) + foreach (Point node in tunnel.Nodes) { - closestDist = dist; - closestWayPoint = wp; - } + float dist = Math.Abs(cell.Center.X - node.X); + if (closestTunnelNode == null || dist < closestDist) + { + closestDist = dist; + closestTunnelNode = node; + } + } } - if (closestWayPoint.WorldPosition.Y < cell.Center.Y) { continue; } + if (closestTunnelNode != null && closestTunnelNode.Value.Y < cell.Center.Y) { continue; } toBeRemoved.Add(cell); } @@ -789,26 +1124,43 @@ namespace Barotrauma return toBeRemoved; } - private void EnlargeMainPath(List pathCells, float minWidth) + private void EnlargePath(List pathCells, float minWidth) + { + if (minWidth <= 0.0f) { return; } + + List removedCells = GetTooCloseCells(pathCells, minWidth); + foreach (VoronoiCell removedCell in removedCells) + { + if (removedCell.CellType == CellType.Path) { continue; } + + pathCells.Add(removedCell); + removedCell.CellType = CellType.Path; + } + } + + private void GenerateWaypoints(Tunnel tunnel, Tunnel parentTunnel) { List wayPoints = new List(); - var newWaypoint = new WayPoint(new Rectangle((int)pathCells[0].Site.Coord.X, borders.Height, 10, 10), null); - wayPoints.Add(newWaypoint); - - for (int i = 0; i < pathCells.Count; i++) + for (int i = 0; i < tunnel.Cells.Count; i++) { - pathCells[i].CellType = CellType.Path; + tunnel.Cells[i].CellType = CellType.Path; - newWaypoint = new WayPoint(new Rectangle((int)pathCells[i].Site.Coord.X, (int)pathCells[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); - - wayPoints[wayPoints.Count-2].linkedTo.Add(newWaypoint); - newWaypoint.linkedTo.Add(wayPoints[wayPoints.Count - 2]); + + if (wayPoints.Count > 1) + { + wayPoints[wayPoints.Count - 2].linkedTo.Add(newWaypoint); + newWaypoint.linkedTo.Add(wayPoints[wayPoints.Count - 2]); + } for (int n = 0; n < wayPoints.Count; n++) { - if (wayPoints[n].Position != newWaypoint.Position) continue; + if (wayPoints[n].Position != newWaypoint.Position) { continue; } wayPoints[n].linkedTo.Add(newWaypoint); newWaypoint.linkedTo.Add(wayPoints[n]); @@ -817,37 +1169,80 @@ namespace Barotrauma } } - newWaypoint = new WayPoint(new Rectangle((int)pathCells[pathCells.Count - 1].Site.Coord.X, borders.Height, 10, 10), null); - wayPoints.Add(newWaypoint); + tunnel.WayPoints.AddRange(wayPoints); - wayPoints[wayPoints.Count - 2].linkedTo.Add(newWaypoint); - newWaypoint.linkedTo.Add(wayPoints[wayPoints.Count - 2]); - - if (minWidth > 0.0f) + //connect to the tunnel we're branching off from + if (parentTunnel != null) { - List removedCells = GetTooCloseCells(pathCells, minWidth); - foreach (VoronoiCell removedCell in removedCells) + var parentStart = FindClosestWayPoint(wayPoints.First(), parentTunnel); + if (parentStart != null) { - if (removedCell.CellType == CellType.Path) continue; - - pathCells.Add(removedCell); - removedCell.CellType = CellType.Path; + wayPoints.First().linkedTo.Add(parentStart); + parentStart.linkedTo.Add(wayPoints.First()); + } + if (tunnel.Type != TunnelType.Cave || tunnel.ParentTunnel.Type == TunnelType.Cave) + { + var parentEnd = FindClosestWayPoint(wayPoints.Last(), parentTunnel); + if (parentEnd != null) + { + wayPoints.Last().linkedTo.Add(parentEnd); + parentEnd.linkedTo.Add(wayPoints.Last()); + } } } } + private void ConnectWaypoints(Tunnel tunnel, Tunnel parentTunnel) + { + foreach (WayPoint wayPoint in tunnel.WayPoints) + { + var closestWaypoint = FindClosestWayPoint(wayPoint, parentTunnel); + if (closestWaypoint == null) { continue; } + if (Submarine.PickBody( + ConvertUnits.ToSimUnits(wayPoint.WorldPosition), + ConvertUnits.ToSimUnits(closestWaypoint.WorldPosition), collisionCategory: Physics.CollisionLevel | Physics.CollisionWall) == null) + { + wayPoint.linkedTo.Add(closestWaypoint); + closestWaypoint.linkedTo.Add(wayPoint); + } + } + } + + private static WayPoint FindClosestWayPoint(WayPoint wayPoint, Tunnel otherTunnel) + { + float closestDist = float.PositiveInfinity; + WayPoint closestWayPoint = null; + foreach (WayPoint otherWayPoint in otherTunnel.WayPoints) + { + float dist = Vector2.DistanceSquared(otherWayPoint.WorldPosition, wayPoint.WorldPosition); + if (dist < closestDist) + { + closestDist = dist; + closestWayPoint = otherWayPoint; + + } + } + return closestWayPoint; + } + private List GetTooCloseCells(List emptyCells, float minDistance) { List tooCloseCells = new List(); - Vector2 position = emptyCells[0].Center; + if (minDistance <= 0.0f) { return tooCloseCells; } - if (minDistance <= 0.0f) return tooCloseCells; + foreach (var cell in emptyCells) + { + foreach (var tooCloseCell in GetTooCloseCells(cell.Center, minDistance)) + { + if (!tooCloseCells.Contains(tooCloseCell)) + { + tooCloseCells.Add(tooCloseCell); + } + } + } - float step = 100.0f; - int targetCellIndex = 1; - - minDistance *= 0.5f; + /*minDistance *= 0.5f; do { tooCloseCells.AddRange(GetTooCloseCells(position, minDistance)); @@ -856,7 +1251,7 @@ namespace Barotrauma if (Vector2.Distance(emptyCells[targetCellIndex].Center, position) < step * 2.0f) targetCellIndex++; - } while (Vector2.Distance(position, emptyCells[emptyCells.Count - 1].Center) > step * 2.0f); + } while (Vector2.Distance(position, emptyCells[emptyCells.Count - 1].Center) > step * 2.0f);*/ return tooCloseCells; } @@ -872,7 +1267,8 @@ 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; @@ -883,25 +1279,7 @@ namespace Barotrauma return tooCloseCells.ToList(); } - - /// - /// remove all cells except those that are adjacent to the empty cells - /// - private List CleanCells(List emptyCells) - { - HashSet newCells = new HashSet(); - foreach (VoronoiCell cell in emptyCells) - { - foreach (GraphEdge edge in cell.Edges) - { - VoronoiCell adjacent = edge.AdjacentCell(cell); - if (adjacent != null) { newCells.Add(adjacent); } - } - } - return newCells.ToList(); - } - - private void GenerateSeaFloor(bool mirror) + private void GenerateSeaFloorPositions(bool mirror) { BottomPos = GenerationParams.SeaFloorDepth; SeaFloorTopPos = BottomPos; @@ -945,6 +1323,10 @@ namespace Barotrauma } SeaFloorTopPos = bottomPositions.Max(p => p.Y); + } + + private void GenerateSeaFloor() + { SeaFloor = new LevelWall(bottomPositions.Select(p => p.ToVector2()).ToList(), new Vector2(0.0f, -2000.0f), GenerationParams.WallColor, this); ExtraWalls.Add(SeaFloor); @@ -959,102 +1341,89 @@ namespace Barotrauma bodies.Add(BottomBarrier); } - private void GenerateTunnels(List pathNodes, int pathWidth) + private void GenerateCaves(Tunnel parentTunnel) { - SmallTunnels = new List>(); - for (int i = 0; i < GenerationParams.SmallTunnelCount; i++) + for (int i = 0; i < GenerationParams.CaveCount; i++) { - var tunnelStartPos = pathNodes[Rand.Range(1, pathNodes.Count - 2, Rand.RandSync.Server)]; + var caveParams = CaveGenerationParams.GetRandom(GenerationParams, 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); - List tunnelNodes = new List() + var cavePos = FindPosAwayFromMainPath(radius, asFarAwayAsPossible: true, allowedArea); + + Point closestParentNode = parentTunnel.Nodes.First(); + double closestDist = double.PositiveInfinity; + foreach (Point node in parentTunnel.Nodes) { - tunnelStartPos, - tunnelStartPos + new Point(0, Math.Sign(tunnelStartPos.Y - Size.Y / 2) * pathWidth * 2) - }; - - List tunnel = GenerateTunnel( - tunnelNodes, - Rand.Range(GenerationParams.SmallTunnelLengthRange.X, GenerationParams.SmallTunnelLengthRange.Y, Rand.RandSync.Server), - pathNodes); - if (tunnel.Any()) SmallTunnels.Add(tunnel); - - int branches = Rand.Range(0, 3, Rand.RandSync.Server); - for (int j = 0; j < branches; j++) - { - List branch = GenerateTunnel( - new List() { tunnel[Rand.Int(tunnel.Count, Rand.RandSync.Server)] }, - Rand.Range(GenerationParams.SmallTunnelLengthRange.X, GenerationParams.SmallTunnelLengthRange.Y, Rand.RandSync.Server) * 0.5f, - pathNodes); - if (branch.Any()) SmallTunnels.Add(branch); - } - - } - } - - private List GenerateTunnel(List tunnelNodes, float tunnelLength, List avoidNodes) - { - int sectionLength = 1000; - - float currLength = 0.0f; - DoubleVector2 dir = null; - while (currLength < tunnelLength) - { - var prevDir = dir; - dir = Rand.Vector(1.0, Rand.RandSync.Server); - - dir.Y += Math.Sign(tunnelNodes[tunnelNodes.Count - 1].Y - Size.Y / 2) * 0.5f; - if (prevDir != null) - { - dir.X = (dir.X + prevDir.X) / 2.0; - dir.Y = (dir.Y + prevDir.Y) / 2.0; - } - - double avoidDist = 20000; - double avoidDistSqr = avoidDist * avoidDist; - foreach (Point pathNode in avoidNodes) - { - double diffX = tunnelNodes[tunnelNodes.Count - 1].X - pathNode.X; - double diffY = tunnelNodes[tunnelNodes.Count - 1].Y - pathNode.Y; - if (Math.Abs(diffX) < 1.0f || Math.Abs(diffY) < 1.0f) continue; - - double distSqr = (diffX * diffX + diffY * diffY); - Debug.Assert(distSqr > 0); - if (distSqr < avoidDistSqr) + double dist = MathUtils.DistanceSquared((double)node.X, (double)node.Y, (double)cavePos.X, (double)cavePos.Y); + if (dist < closestDist) { - double dist = Math.Sqrt(distSqr); - - dir.X += (diffX / dist) * (1.0f - dist / avoidDist); - dir.Y += (diffY / dist) * (1.0f - dist / avoidDist); + closestParentNode = node; + closestDist = dist; } } - dir.Normalize(); + 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); - if (tunnelNodes.Last().Y + dir.Y > Size.Y) + 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++) { - //head back down if the tunnel has reached the top of the level - dir.Y = -dir.Y; - } - else if (tunnelNodes.Last().Y + dir.Y * 500 < 500) - { - //head back up if reached the bottom of the level - dir.Y = -dir.Y; - } - else if (tunnelNodes.Last().Y + dir.Y + dir.Y < 0.0f || - tunnelNodes.Last().Y + dir.Y + dir.Y < SeaFloorTopPos) - { - //head back up if reached the sea floor - dir.Y = -dir.Y; + 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); } - Point nextNode = tunnelNodes.Last() + new Point((int)(dir.X * sectionLength), (int)(dir.Y * sectionLength)); - nextNode.X = MathHelper.Clamp(nextNode.X, 500, Size.X - 500); - nextNode.Y = MathHelper.Clamp(nextNode.Y, SeaFloorTopPos, Size.Y - 500); - tunnelNodes.Add(nextNode); - currLength += sectionLength; + 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; + } + + CalculateTunnelDistanceField(density: 1000); } - - return tunnelNodes; } private void GenerateRuin(List mainPath, bool mirror) @@ -1065,135 +1434,9 @@ namespace Barotrauma Rand.Range(ruinGenerationParams.SizeMin.X, ruinGenerationParams.SizeMax.X, Rand.RandSync.Server), Rand.Range(ruinGenerationParams.SizeMin.Y, ruinGenerationParams.SizeMax.Y, Rand.RandSync.Server)); int ruinRadius = Math.Max(ruinSize.X, ruinSize.Y) / 2; - - int cellIndex = Rand.Int(cells.Count, Rand.RandSync.Server); - Point ruinPos = new Point((int)cells[cellIndex].Site.Coord.X, (int)cells[cellIndex].Site.Coord.X); - //50% chance of placing the ruins at a cave - if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) < 0.5f) - { - TryGetInterestingPosition(true, PositionType.Cave, 0.0f, out ruinPos); - } - - ruinPos.Y = Math.Min(ruinPos.Y, borders.Y + borders.Height - ruinSize.Y / 2); - ruinPos.Y = Math.Max(ruinPos.Y, SeaFloorTopPos + ruinSize.Y / 2); - - double minMainPathDist = ruinRadius * 2; - double minMainPathDistSqr = minMainPathDist * minMainPathDist; - - double minOutpostDist = Math.Min(Math.Min(10000.0f, Size.X / 3), Size.Y / 3); - double minOutpostDistSqr = minOutpostDist * minOutpostDist; - - int iter = 0; - while (mainPath.Any(p => MathUtils.DistanceSquared(ruinPos.X, ruinPos.Y, p.Site.Coord.X, p.Site.Coord.Y) < minMainPathDistSqr) || - Ruins.Any(r => r.Area.Intersects(new Rectangle(ruinPos - new Point(ruinSize.X / 2, ruinSize.Y / 2), ruinSize)) || - MathUtils.DistanceSquared(ruinPos.X, ruinPos.Y, StartPosition.X, StartPosition.Y) < minOutpostDistSqr || - MathUtils.DistanceSquared(ruinPos.X, ruinPos.Y, StartPosition.X, Size.Y) < minOutpostDistSqr || - MathUtils.DistanceSquared(ruinPos.X, ruinPos.Y, EndPosition.X, EndPosition.Y) < minOutpostDistSqr) || - MathUtils.DistanceSquared(ruinPos.X, ruinPos.Y, EndPosition.X, Size.Y) < minOutpostDistSqr) - { - double weighedPathPosX = ruinPos.X; - double weighedPathPosY = ruinPos.Y; - iter++; - - for (int i = 0; i < 2; i++) - { - double diffX = i == 0 ? ruinPos.X - StartPosition.X : ruinPos.Y - StartPosition.X; - double diffY = i == 0 ? ruinPos.Y - StartPosition.Y : ruinPos.Y - StartPosition.Y; - - double distSqr = diffX * diffX + diffY * diffY; - if (distSqr < minMainPathDistSqr) - { - double dist = Math.Sqrt(distSqr); - double moveAmountX = minMainPathDist * diffX / dist; - double moveAmountY = minMainPathDist * diffY / dist; - weighedPathPosX += moveAmountX; - weighedPathPosY += moveAmountY; - weighedPathPosY = Math.Min(borders.Y + borders.Height - ruinSize.Y / 2, weighedPathPosY); - } - } - - foreach (VoronoiCell pathCell in mainPath) - { - double diffX = ruinPos.X - pathCell.Site.Coord.X; - double diffY = ruinPos.Y - pathCell.Site.Coord.Y; - - double distSqr = diffX * diffX + diffY * diffY; - if (distSqr < 1.0) - { - diffX = 0; - diffY = 1; - distSqr = 1.0; - } - if (distSqr > 10000.0 * 10000.0) continue; - - double dist = Math.Sqrt(distSqr); - double moveAmountX = 100.0 * diffX / dist; - double moveAmountY = 100.0 * diffY / dist; - - weighedPathPosX += moveAmountX; - weighedPathPosY += moveAmountY; - weighedPathPosY = Math.Min(borders.Y + borders.Height - ruinSize.Y / 2, weighedPathPosY); - } - - Rectangle ruinArea = new Rectangle(ruinPos - new Point(ruinSize.X / 2, ruinSize.Y / 2), ruinSize); - foreach (Ruin otherRuin in Ruins) - { - if (!otherRuin.Area.Intersects(ruinArea)) continue; - - double diffX = ruinArea.Center.X - otherRuin.Area.Center.X; - double diffY = ruinArea.Center.Y - otherRuin.Area.Center.Y; - - double distSqr = diffX * diffX + diffY * diffY; - if (distSqr < 0.01f) - { - diffX = 0; - diffY = -1; - distSqr = 1; - } - - double dist = Math.Sqrt(distSqr); - double moveAmountX = diffX / dist; - double moveAmountY = diffY / dist; - - int move = (Math.Max(ruinArea.Width, ruinArea.Height) + Math.Max(otherRuin.Area.Width, otherRuin.Area.Height)) / 2; - moveAmountX *= move; - moveAmountY *= move; - - weighedPathPosX += moveAmountX; - weighedPathPosY += moveAmountY; - } - ruinPos = new Point((int)weighedPathPosX, (int)weighedPathPosY); - - //if we can't find a suitable position after 10 000 iterations, give up - if (iter > 10000) - { - if (Ruins.Count > 0) - { - //we already have some ruins, don't add this one at all - return; - } - string errorMsg = "Failed to find a suitable position for ruins. Level seed: " + Seed + - ", ruin size: " + ruinSize + ", selected sub " + (Submarine.MainSub == null ? "none" : Submarine.MainSub.Info.Name); - DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("Level.GenerateRuins:PosNotFound", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); - break; - } - //if we haven't found a position after 500 iterations, try another starting point - else if (iter > 500 && iter % 500 == 0) - { - int newCellIndex = Rand.Int(cells.Count, Rand.RandSync.Server); - ruinPos = new Point((int)cells[newCellIndex].Site.Coord.X, (int)cells[newCellIndex].Site.Coord.X); - } - ruinPos.Y = Math.Min(ruinPos.Y, borders.Y + borders.Height - ruinSize.Y / 2); - ruinPos.Y = Math.Max(ruinPos.Y, SeaFloorTopPos + ruinSize.Y / 2); - } - - if (Math.Abs(ruinPos.X) > int.MaxValue / 2 || Math.Abs(ruinPos.Y) > int.MaxValue / 2) - { - DebugConsole.ThrowError("Something went wrong during ruin generation. Ruin position: " + ruinPos); - return; - } + Point ruinPos = FindPosAwayFromMainPath(ruinRadius + Tunnels.First().MinWidth, asFarAwayAsPossible: false, + limits: new Rectangle(new Point(ruinSize.X / 2, ruinSize.Y / 2), Size - ruinSize)); VoronoiCell closestPathCell = null; double closestDist = 0.0f; @@ -1236,7 +1479,8 @@ namespace Barotrauma foreach (VoronoiCell cell in tooClose) { - if (cell.CellType == CellType.Empty) continue; + if (cell.CellType == CellType.Empty) { continue; } + if (ExtraWalls.Any(w => w.Cells.Contains(cell))) { continue; } foreach (GraphEdge e in cell.Edges) { Rectangle rect = ruinShape.Rect; @@ -1245,9 +1489,13 @@ namespace Barotrauma MathUtils.GetLineRectangleIntersection(e.Point1, e.Point2, rect, out _)) { cell.CellType = CellType.Removed; - int x = (int)Math.Floor(cell.Center.X / GridCellSize); - int y = (int)Math.Floor(cell.Center.Y / GridCellSize); - cellGrid[x, y].Remove(cell); + for (int x = 0; x < cellGrid.GetLength(0); x++) + { + for (int y = 0; y < cellGrid.GetLength(1); y++) + { + cellGrid[x, y].Remove(cell); + } + } cells.Remove(cell); break; } @@ -1255,80 +1503,769 @@ namespace Barotrauma } } - //cast a ray from the closest path cell towards the ruin and remove the cell it hits - //to ensure that there's always at least one way from the main tunnel to the ruin - List validCells = cells.FindAll(c => c.CellType != CellType.Empty && c.CellType != CellType.Removed); - foreach (VoronoiCell cell in validCells) + CreatePathToClosestTunnel(ruinPos); + } + + private Point FindPosAwayFromMainPath(double minDistance, bool asFarAwayAsPossible, 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()) { - foreach (GraphEdge e in cell.Edges) + if (!validPoints.Any()) { validPoints = distanceField; } + Pair furthestPoint = null; + foreach (var point in validPoints) { - if (MathUtils.LinesIntersect(closestPathCell.Center, ruinPos.ToVector2(), e.Point1, e.Point2)) + if (furthestPoint == null || point.Second > furthestPoint.Second) { - cell.CellType = CellType.Removed; - int x = (int)Math.Floor(cell.Center.X / GridCellSize); - int y = (int)Math.Floor(cell.Center.Y / GridCellSize); - cellGrid[x, y].Remove(cell); - cells.Remove(cell); - break; + furthestPoint = point; } } - if (cell.CellType == CellType.Removed) + return furthestPoint.First; + } + else + { + return validPoints[Rand.Int(validPoints.Count, Rand.RandSync.Server)].First; + } + } + + private void CalculateTunnelDistanceField(int density) + { + distanceField = new List>(); + for (int x = 0; x < Size.X; x += density) + { + for (int y = 0; y < Size.Y; y += density) { - break; + Point point = new Point(x, y); + double shortestDistSqr = double.PositiveInfinity; + foreach (Tunnel tunnel in Tunnels) + { + for (int i = 1; i < tunnel.Nodes.Count; i++) + { + shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.LineSegmentToPointDistanceSquared(tunnel.Nodes[i - 1], tunnel.Nodes[i], point)); + } + } + shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.DistanceSquared((double)point.X, (double)point.Y, (double)startPosition.X, (double)startPosition.Y)); + shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.DistanceSquared((double)point.X, (double)point.Y, (double)endPosition.X, (double)endPosition.Y)); + distanceField.Add(new Pair(point, Math.Sqrt(shortestDistSqr))); } } } + private double GetDistToTunnel(Vector2 position, Tunnel tunnel) + { + Point point = position.ToPoint(); + double shortestDistSqr = double.PositiveInfinity; + for (int i = 1; i < tunnel.Nodes.Count; i++) + { + shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.LineSegmentToPointDistanceSquared(tunnel.Nodes[i - 1], tunnel.Nodes[i], point)); + } + return Math.Sqrt(shortestDistSqr); + } + + private DestructibleLevelWall CreateIceChunk(IEnumerable edges, Vector2 position, float? health = null) + { + List vertices = new List(); + foreach (GraphEdge edge in edges) + { + if (!vertices.Any()) + { + vertices.Add(edge.Point1); + } + else if (!vertices.Any(v => v.NearlyEquals(edge.Point1))) + { + vertices.Add(edge.Point1); + } + else if (!vertices.Any(v => v.NearlyEquals(edge.Point2))) + { + vertices.Add(edge.Point2); + } + } + return CreateIceChunk(vertices.Select(v => v - position).ToList(), position, health); + } + + private DestructibleLevelWall CreateIceChunk(List vertices, Vector2 position, float? health = null) + { + DestructibleLevelWall newChunk = new DestructibleLevelWall(vertices, Color.White, this, health, true); + newChunk.Body.Position = ConvertUnits.ToSimUnits(position); + newChunk.Cells.ForEach(c => c.Translation = position); + newChunk.Body.BodyType = BodyType.Dynamic; + newChunk.Body.FixedRotation = true; + newChunk.Body.LinearDamping = 0.5f; + newChunk.Body.IgnoreGravity = true; + newChunk.Body.Mass *= 10.0f; + return newChunk; + } + + 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; + VoronoiCell closestCell = null; + foreach (VoronoiCell cell in cells) + { + if (cell.CellType != CellType.Solid) { 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) + { + closestDistSqr = distSqr; + closestEdge = edge; + closestCell = cell; + } + } + } + + if (closestEdge == null) { return null; } + + usedSpireEdges.Add(closestEdge); + + Vector2 edgeNormal = closestEdge.GetNormal(closestCell); + float spireLength = (float)Math.Min(Math.Sqrt(closestDistSqr), maxLength); + 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() + { + closestEdge.Point1, + extrudedPoint1 + (extrudedPoint2 - extrudedPoint1) * Rand.Range(0.3f, 0.45f, Rand.RandSync.Server), + extrudedPoint2 + (extrudedPoint1 - extrudedPoint2) * Rand.Range(0.3f, 0.45f, Rand.RandSync.Server), + closestEdge.Point2, + }; + Vector2 center = Vector2.Zero; + vertices.ForEach(v => center += v); + center /= vertices.Count; + DestructibleLevelWall spire = new DestructibleLevelWall(vertices.Select(v => v - center).ToList(), Color.White, this, health: 100.0f, giftWrap: true); +#if CLIENT + //make the edge at the bottom of the spire non-solid + foreach (GraphEdge edge in spire.Cells[0].Edges) + { + if ((edge.Point1.NearlyEquals(closestEdge.Point1 - center) && edge.Point2.NearlyEquals(closestEdge.Point2 - center)) || + (edge.Point1.NearlyEquals(closestEdge.Point2 - center) && edge.Point2.NearlyEquals(closestEdge.Point1 - center))) + { + edge.IsSolid = false; + break; + } + } + spire.GenerateVertices(); +#endif + spire.Body.Position = ConvertUnits.ToSimUnits(center); + spire.Body.BodyType = BodyType.Static; + spire.Body.FixedRotation = true; + spire.Body.IgnoreGravity = true; + spire.Body.Mass *= 10.0f; + spire.Cells.ForEach(c => c.Translation = center); + spire.WallDamageOnTouch = 50.0f; + return spire; + } + + // TODO: Improve this temporary level editor debug solution (or remove it) + private static int nextPathPointId; + public List PathPoints { get; } = new List(); + public struct PathPoint + { + public string Id { get; } + public Vector2 Position { get; } + public bool ShouldContainResources { get; set; } + public float NextClusterProbability + { + get + { + return ClusterLocations.Count switch + { + 1 => 5.0f, + 2 => 2.5f, + 3 => 1.0f, + _ => 0.0f, + }; + } + } + public List ResourceTags { get; } + public List ResourceIds { get; } + public List ClusterLocations { get; } + public TunnelType TunnelType { get; } + + public PathPoint(string id, Vector2 position, bool shouldContainResources, TunnelType tunnelType) + { + Id = id; + Position = position; + ShouldContainResources = shouldContainResources; + ResourceTags = new List(); + ResourceIds = new List(); + ClusterLocations = new List(); + TunnelType = tunnelType; + } + } + + public struct ClusterLocation + { + public VoronoiCell Cell { get; } + public GraphEdge Edge { get; } + public Vector2 EdgeCenter { get; } + /// + /// Can be null unless initialized in constructor + /// + public List Resources { get; private set; } + + /// List is initialized only when specified, otherwise will be null + public ClusterLocation(VoronoiCell cell, GraphEdge edge, bool initializeResourceList = false) + { + Cell = cell; + Edge = edge; + EdgeCenter = edge.Center; + Resources = initializeResourceList ? new List() : null; + } + + public bool Equals(ClusterLocation anotherLocation) => + Cell == anotherLocation.Cell && Edge == anotherLocation.Edge; + + public bool Equals(VoronoiCell cell, GraphEdge edge) => + Cell == cell && Edge == edge; + + public void InitializeResources() + { + Resources = new List(); + } + } + + // TODO: Take into account items which aren't ores or plants + // Such as the exploding crystals in The Great Sea private void GenerateItems() { string levelName = GenerationParams.Identifier.ToLowerInvariant(); - List> levelItems = new List>(); + float minCommonness = float.MaxValue, maxCommonness = float.MinValue; + List> levelResources = new List>(); + var fixedResources = new List>(); foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs) { if (itemPrefab.LevelCommonness.TryGetValue(levelName, out float commonness) || itemPrefab.LevelCommonness.TryGetValue("", out commonness)) { - levelItems.Add(new Pair(itemPrefab, commonness)); + if (commonness <= 0.0f) { continue; } + if (commonness < minCommonness) { minCommonness = commonness; } + if (commonness > maxCommonness) { maxCommonness = commonness; } + levelResources.Add(new Pair(itemPrefab, commonness)); + } + else if (itemPrefab.LevelQuantity.TryGetValue(levelName, out var fixedQuantityResourceInfo) || + itemPrefab.LevelQuantity.TryGetValue("", out fixedQuantityResourceInfo)) + { + fixedResources.Add(new Tuple(itemPrefab, fixedQuantityResourceInfo)); } } DebugConsole.Log("Generating level resources..."); + var allValidLocations = GetAllValidClusterLocations(); + var maxResourceOverlap = 0.4f; - for (int i = 0; i < GenerationParams.ItemCount; i++) + foreach (var fixedResource in fixedResources) { - var selectedPrefab = ToolBox.SelectWeightedRandom( - levelItems.Select(it => it.First).ToList(), - levelItems.Select(it => it.Second).ToList(), - Rand.RandSync.Server); - if (selectedPrefab == null) { break; } - - var selectedCell = cells[Rand.Int(cells.Count, Rand.RandSync.Server)]; - var selectedEdge = selectedCell.Edges.GetRandom(e => e.IsSolid && !e.OutsideLevel, Rand.RandSync.Server); - if (selectedEdge == null) continue; - - - float edgePos = Rand.Range(0.0f, 1.0f, Rand.RandSync.Server); - Vector2 selectedPos = Vector2.Lerp(selectedEdge.Point1, selectedEdge.Point2, edgePos); - Vector2 edgeNormal = selectedEdge.GetNormal(selectedCell); - - var item = new Item(selectedPrefab, selectedPos, submarine: null); - item.Move(edgeNormal * item.Rect.Height / 2, ignoreContacts: true); - - var holdable = item.GetComponent(); - if (holdable == null) + for (int i = 0; i < fixedResource.Item2.ClusterQuantity; i++) { - DebugConsole.ThrowError("Error while placing items in the level - item \"" + item.Name + "\" is not holdable and cannot be attached to the level walls."); + var location = allValidLocations.GetRandom(l => + { + if (l.Cell == null || l.Edge == null) { return false; } + if (fixedResource.Item2.IsIslandSpecifc && !l.Cell.Island) { return false; } + return fixedResource.Item2.ClusterSize <= GetMaxResourcesOnEdge(fixedResource.Item1, l, out _); + + }, randSync: Rand.RandSync.Server); + + if (location.Cell == null || location.Edge == null) { break; } + + PlaceResources(fixedResource.Item1, fixedResource.Item2.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; + for (int i = 1; i < tunnel.Nodes.Count; i++) + { + tunnelLength += Vector2.Distance(tunnel.Nodes[i - 1].ToVector2(), tunnel.Nodes[i].ToVector2()); + } + + var nextNodeIndex = 1; + var positionOnPath = tunnel.Nodes.First().ToVector2(); + var lastNodePos = tunnel.Nodes.Last().ToVector2(); + var reachedLastNode = false; + var intervalRange = tunnel.Type != TunnelType.Cave ? GenerationParams.ResourceIntervalRange : GenerationParams.CaveResourceIntervalRange; + do + { + var distance = Rand.Range(intervalRange.X, intervalRange.Y, sync: Rand.RandSync.Server); + reachedLastNode = !CalculatePositionOnPath(); + var id = Tunnels.IndexOf(tunnel) + ":" + nextPathPointId++; + var spawnChance = tunnel.Type == TunnelType.Cave || tunnel.ParentTunnel?.Type == TunnelType.Cave ? + GenerationParams.CaveResourceSpawnChance : GenerationParams.ResourceSpawnChance; + var containsResources = true; + if (spawnChance < 1.0f) + { + var spawnPointRoll = Rand.Range(0.0f, 1.0f, sync: Rand.RandSync.Server); + containsResources = spawnPointRoll <= spawnChance; + } + var tunnelType = tunnel.Type; + if (tunnel.ParentTunnel != null && tunnel.ParentTunnel.Type == TunnelType.Cave) { tunnelType = TunnelType.Cave; } + PathPoints.Add(new PathPoint(id, positionOnPath, containsResources, tunnel.Type)); + + bool CalculatePositionOnPath(float checkedDist = 0.0f) + { + if (nextNodeIndex >= tunnel.Nodes.Count) { return false; } + var distToNextNode = Vector2.Distance(positionOnPath, tunnel.Nodes[nextNodeIndex].ToVector2()); + var lerpAmount = (distance - checkedDist) / distToNextNode; + if (lerpAmount <= 1.0f) + { + positionOnPath = Vector2.Lerp(positionOnPath, tunnel.Nodes[nextNodeIndex].ToVector2(), lerpAmount); + return true; + } + else + { + positionOnPath = tunnel.Nodes[nextNodeIndex++].ToVector2(); + return CalculatePositionOnPath(checkedDist + distToNextNode); + } + } + } while (!reachedLastNode && Vector2.DistanceSquared(positionOnPath, lastNodePos) > (intervalRange.Y * intervalRange.Y)); + } + + int itemCount = 0; + string[] exclusiveResourceTags = new string[2] { "ore", "plant" }; + + // Create first cluster for each spawn point + foreach (var pathPoint in PathPoints.Where(p => p.ShouldContainResources)) + { + if (itemCount >= GenerationParams.ItemCount) { break; } + GenerateFirstCluster(pathPoint); + } + + // Don't try to spawn more resource clusters for points + // for which the initial cluster could not be spawned + PathPoints.Where(p => p.ShouldContainResources && p.ClusterLocations.Count == 0) + .ForEach(p => p.ShouldContainResources = false); + + var excludedPathPointIds = new List(); + while (itemCount < GenerationParams.ItemCount) + { + var availablePathPoints = PathPoints.Where(p => + p.ShouldContainResources && p.NextClusterProbability > 0 && + !excludedPathPointIds.Contains(p.Id)); + + if (availablePathPoints.None()) { break; } + + var pathPoint = ToolBox.SelectWeightedRandom( + availablePathPoints.ToList(), + availablePathPoints.Select(p => p.NextClusterProbability).ToList(), + Rand.RandSync.Server); + + GenerateAdditionalCluster(pathPoint); + } + + // If none of the point set to contain resources can take more resources, + // but we still haven't reached the item count set in the generation parameters... + while (itemCount < GenerationParams.ItemCount) + { + // We need to start filling some of the path points previously set to not contain resources + var availablePathPoints = PathPoints.Where(p => !excludedPathPointIds.Contains(p.Id) && p.ClusterLocations.None()); + if (availablePathPoints.None()) { break; } + var pathPoint = availablePathPoints.GetRandom(randSync: Rand.RandSync.Server); + if (!GenerateFirstCluster(pathPoint)) + { + excludedPathPointIds.Add(pathPoint.Id); + continue; + } + while (pathPoint.NextClusterProbability > 0) + { + if (!GenerateAdditionalCluster(pathPoint)) { break; } + } + pathPoint.ShouldContainResources = pathPoint.ClusterLocations.Any(); + } + +#if DEBUG + DebugConsole.NewMessage("Level resources spawned: " + itemCount + "\n" + + "Spawn points containing resources: " + PathPoints.Where(p => p.ClusterLocations.Any()).Count() + "/" + PathPoints.Count); +#endif + + DebugConsole.Log("Level resources generated"); + + bool GenerateFirstCluster(PathPoint pathPoint) + { + var intervalRange = pathPoint.TunnelType != TunnelType.Cave ? + GenerationParams.ResourceIntervalRange : GenerationParams.CaveResourceIntervalRange; + allValidLocations.Sort((x, y) => + Vector2.DistanceSquared(pathPoint.Position, x.EdgeCenter) + .CompareTo(Vector2.DistanceSquared(pathPoint.Position, y.EdgeCenter))); + var selectedLocationIndex = -1; + var generatedCluster = false; + for (int i = 0; i < allValidLocations.Count; i++) + { + var validLocation = allValidLocations[i]; + if (!IsNextToTunnelType(validLocation.Edge, pathPoint.TunnelType)) { 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; } + // Edge is closer to the path point than the cell center + if (distanceSquaredToEdge > Vector2.DistanceSquared(pathPoint.Position, validLocation.Cell.Center)) { continue; } + + var validComparedToOtherPathPoints = true; + // Make sure this path point is closest to 'validLocation' + foreach (var anotherPathPoint in PathPoints) + { + if (anotherPathPoint.Id == pathPoint.Id) { continue; } + if (Vector2.DistanceSquared(anotherPathPoint.Position, validLocation.EdgeCenter) < distanceSquaredToEdge) + { + validComparedToOtherPathPoints = false; + break; + } + } + + foreach (var anotherPathPoint in PathPoints.Where(p => p.Id != pathPoint.Id && p.ClusterLocations.Any())) + { + if (!validComparedToOtherPathPoints) { break; } + foreach (var c in pathPoint.ClusterLocations) + { + if (IsInvalidComparedToExistingLocation()) + { + validComparedToOtherPathPoints = false; + break; + } + + bool IsInvalidComparedToExistingLocation() + { + if (c.Equals(validLocation)) { return true; } + // If there is a previously spawned cluster too near + if (Vector2.DistanceSquared(c.EdgeCenter, validLocation.EdgeCenter) > (intervalRange.X * intervalRange.X)) { return true; } + // If there is a line from a previous path point to one of its existing cluster locations + // which intersects with the line from this path point to the new possible cluster location + if (MathUtils.LinesIntersect(anotherPathPoint.Position, c.EdgeCenter, pathPoint.Position, validLocation.EdgeCenter)) { return true; } + return false; + } + } + } + + if (!validComparedToOtherPathPoints) { continue; } + generatedCluster = CreateResourceCluster(pathPoint, validLocation); + selectedLocationIndex = i; + break; + } + + if (selectedLocationIndex >= 0) + { + allValidLocations.RemoveAt(selectedLocationIndex); + } + + return generatedCluster; + + static bool IsNextToTunnelType(GraphEdge e, TunnelType t) => + (e.NextToMainPath && t == TunnelType.MainPath) || + (e.NextToSidePath && t == TunnelType.SidePath) || + (e.NextToCave && t == TunnelType.Cave); + } + + bool GenerateAdditionalCluster(PathPoint pathPoint) + { + var validLocations = new List(); + // First check only the edges of the same cell + // which are connected to one of the existing edges with clusters + foreach (var clusterLocation in pathPoint.ClusterLocations) + { + foreach (var anotherEdge in clusterLocation.Cell.Edges.Where(e => e != clusterLocation.Edge)) + { + if (HaveConnectingEdgePoints(anotherEdge, clusterLocation.Edge)) + { + AddIfValid(clusterLocation.Cell, anotherEdge); + } + } + } + + // Only check edges of adjacent cells if no valid edges were found + // on any of the cells with existing clusters + if (validLocations.None()) + { + foreach (var clusterLocation in pathPoint.ClusterLocations) + { + foreach (var anotherEdge in clusterLocation.Cell.Edges.Where(e => e != clusterLocation.Edge)) + { + var adjacentCell = anotherEdge.AdjacentCell(clusterLocation.Cell); + if (adjacentCell == null) { continue; } + foreach (var adjacentCellEdge in adjacentCell.Edges.Where(e => e != anotherEdge)) + { + if (HaveConnectingEdgePoints(adjacentCellEdge, clusterLocation.Edge)) + { + AddIfValid(adjacentCell, adjacentCellEdge); + } + } + } + } + } + + if (validLocations.Any()) + { + var location = validLocations.GetRandom(randSync: Rand.RandSync.Server); + if (CreateResourceCluster(pathPoint, location)) + { + var i = allValidLocations.FindIndex(l => l.Equals(location)); + if (i >= 0) + { + allValidLocations.RemoveAt(i); + } + return true; + } + else + { + excludedPathPointIds.Add(pathPoint.Id); + return false; + } } else { - holdable.AttachToWall(); + excludedPathPointIds.Add(pathPoint.Id); + return false; + } + + static bool HaveConnectingEdgePoints(GraphEdge e1, GraphEdge e2) => + e1.Point1.NearlyEquals(e2.Point1) || e1.Point1.NearlyEquals(e2.Point2) || + e1.Point2.NearlyEquals(e2.Point1) || e1.Point2.NearlyEquals(e2.Point2); + + void AddIfValid(VoronoiCell c, GraphEdge e) + { + if (IsAlreadyInList(e)) { return; } + if (allValidLocations.None(l => l.Equals(c, e))) { return; } + if (pathPoint.ClusterLocations.Any(cl => cl.Edge == e)) { return; } + validLocations.Add(new ClusterLocation(c, e)); + } + + bool IsAlreadyInList(GraphEdge edge) => + validLocations.Any(l => l.Edge == edge); + } + + bool CreateResourceCluster(PathPoint pathPoint, ClusterLocation location) + { + if (location.Cell == null || location.Edge == null) { return false; } + + ItemPrefab selectedPrefab; + if (pathPoint.ClusterLocations.Count == 0) + { + selectedPrefab = ToolBox.SelectWeightedRandom( + levelResources.Select(it => it.First).ToList(), + levelResources.Select(it => it.Second).ToList(), + Rand.RandSync.Server); + selectedPrefab.Tags.ForEach(t => + { + if (exclusiveResourceTags.Contains(t)) + { + pathPoint.ResourceTags.Add(t); + } + }); + } + else + { + var filteredResources = levelResources.Where(it => + !pathPoint.ResourceIds.Contains(it.First.Identifier) && + pathPoint.ResourceTags.Any() && it.First.Tags.Any(t => pathPoint.ResourceTags.Contains(t))); + selectedPrefab = ToolBox.SelectWeightedRandom( + filteredResources.Select(it => it.First).ToList(), + filteredResources.Select(it => it.Second).ToList(), + Rand.RandSync.Server); + } + + if (selectedPrefab == null) { return false; } + + // Create resources for the cluster + var commonness = levelResources.First(r => r.First == selectedPrefab).Second; + var lerpAmount = MathUtils.InverseLerp(minCommonness, maxCommonness, commonness); + var maxClusterSize = (int)MathHelper.Lerp(GenerationParams.ResourceClusterSizeRange.X, GenerationParams.ResourceClusterSizeRange.Y, lerpAmount); + var maxFitOnEdge = GetMaxResourcesOnEdge(selectedPrefab, location, out var edgeLength); + maxClusterSize = Math.Min(maxClusterSize, maxFitOnEdge); + if (itemCount + maxClusterSize > GenerationParams.ItemCount) + { + maxClusterSize += GenerationParams.ItemCount - (itemCount + maxClusterSize); + } + + if (maxClusterSize < 1) { return false; } + + var minClusterSize = Math.Min(GenerationParams.ResourceClusterSizeRange.X, maxClusterSize); + var resourcesInCluster = maxClusterSize == 1 ? 1 : Rand.Range(minClusterSize, maxClusterSize + 1, sync: Rand.RandSync.Server); + + if (resourcesInCluster < 1) { return false; } + + PlaceResources(selectedPrefab, resourcesInCluster, location, out var placedResources, edgeLenght: edgeLength); + itemCount += resourcesInCluster; + location.InitializeResources(); + location.Resources.AddRange(placedResources); + pathPoint.ClusterLocations.Add(location); + pathPoint.ResourceIds.Add(selectedPrefab.Identifier); + + 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 + public List GenerateMissionResources(ItemPrefab prefab, int requiredAmount, out float rotation) + { + var allValidLocations = GetAllValidClusterLocations(); + var placedResources = new List(); + rotation = 0.0f; + if (allValidLocations.None()) { return placedResources; } // TODO: WHAT?! + + for (int i = allValidLocations.Count - 1; i >= 0; i--) + { + var location = allValidLocations[i]; + var locationHasResources = PathPoints.Any(p => + p.ClusterLocations.Any(c => + c.Equals(location) && + c.Resources.Any(r => r != null && !r.Removed && + (!(r.GetComponent() is Holdable h) || (h.Attachable && h.Attached))))); + if(locationHasResources) + { + allValidLocations.RemoveAt(i); + } + } + + var positionType = PositionType.MainPath; + if (PositionsOfInterest.Any(p => p.PositionType == PositionType.Cave)) + { + positionType = PositionType.Cave; + } + else if (PositionsOfInterest.Any(p => p.PositionType == PositionType.SidePath)) + { + positionType = PositionType.SidePath; + } + + var poi = PositionsOfInterest.GetRandom(p => p.PositionType == positionType, randSync: Rand.RandSync.Server); + var poiPos = poi.Position.ToVector2(); + allValidLocations.Sort((x, y) => Vector2.DistanceSquared(poiPos, x.EdgeCenter) + .CompareTo(Vector2.DistanceSquared(poiPos, y.EdgeCenter))); + var maxResourceOverlap = 0.4f; + // TODO: Find multiple locations if there's too many resources to fit on a sigle edge + var selectedLocation = allValidLocations.FirstOrDefault(l => + Vector2.Distance(l.Edge.Point1, l.Edge.Point2) is float edgeLength && + requiredAmount <= (int)Math.Floor(edgeLength / ((1.0f - maxResourceOverlap) * prefab.Size.X))); + PlaceResources(prefab, requiredAmount, selectedLocation, out placedResources); + var edgeNormal = selectedLocation.Edge.GetNormal(selectedLocation.Cell); + rotation = MathHelper.ToDegrees(-MathUtils.VectorToAngle(edgeNormal) + MathHelper.PiOver2); + return placedResources; + } + + private List GetAllValidClusterLocations() + { + var subBorders = new List(); + Wrecks.ForEach(w => AddBordersToList(w)); + AddBordersToList(BeaconStation); + + var locations = new List(); + foreach (var c in GetAllCells()) + { + if (c.CellType != CellType.Solid) { continue; } + foreach (var e in c.Edges) + { + if (IsValidEdge(e)) + { + locations.Add(new ClusterLocation(c, e)); + } + } + } + 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; } + 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) + { + edgeLenght ??= Vector2.Distance(location.Edge.Point1, location.Edge.Point2); + var minResourceOverlap = -((edgeLenght.Value - (resourceCount * resourcePrefab.Size.X)) / (resourceCount * resourcePrefab.Size.X)); + minResourceOverlap = Math.Max(minResourceOverlap, 0.0f); + var lerpAmounts = new float[resourceCount]; + lerpAmounts[0] = 0.0f; + var lerpAmount = 0.0f; + 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; + lerpAmounts[i] = Math.Clamp(lerpAmount, 0.0f, 1.0f); + } + var startOffset = Rand.Range(0.0f, 1.0f - lerpAmount, sync: Rand.RandSync.Server); + placedResources = new List(); + for (int i = 0; i < resourceCount; i++) + { + Vector2 selectedPos = Vector2.Lerp(location.Edge.Point1, location.Edge.Point2, startOffset + lerpAmounts[i]); + var item = new Item(resourcePrefab, selectedPos, submarine: null); + Vector2 edgeNormal = location.Edge.GetNormal(location.Cell); + item.Move(edgeNormal * (item.body == null ? item.Rect.Height / 2 : ConvertUnits.ToDisplayUnits(item.body.GetMaxExtent() * 0.7f)), ignoreContacts: true); + if (item.GetComponent() is Holdable h) + { + h.AttachToWall(); #if CLIENT 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); } - - DebugConsole.Log("Level resources generated"); } public Vector2 GetRandomItemPos(PositionType spawnPosType, float randomSpread, float minDistFromSubs, float offsetFromWall = 10.0f) @@ -1343,7 +2280,7 @@ namespace Barotrauma int tries = 0; do { - Loaded.TryGetInterestingPosition(true, spawnPosType, minDistFromSubs, out Vector2 startPos); + TryGetInterestingPosition(true, spawnPosType, minDistFromSubs, out Vector2 startPos); Vector2 offset = Rand.Vector(Rand.Range(0.0f, randomSpread, Rand.RandSync.Server), Rand.RandSync.Server); if (!cells.Any(c => c.IsPointInside(startPos + offset))) @@ -1356,7 +2293,8 @@ namespace Barotrauma if (Submarine.PickBody( ConvertUnits.ToSimUnits(startPos), ConvertUnits.ToSimUnits(endPos), - null, Physics.CollisionLevel | Physics.CollisionWall) != null) + 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; break; @@ -1374,7 +2312,6 @@ namespace Barotrauma return position; } - public bool TryGetInterestingPosition(bool useSyncedRand, PositionType positionType, float minDistFromSubs, out Vector2 position) { bool success = TryGetInterestingPosition(useSyncedRand, positionType, minDistFromSubs, out Point pos); @@ -1392,7 +2329,7 @@ namespace Barotrauma List suitablePositions = PositionsOfInterest.FindAll(p => positionType.HasFlag(p.PositionType)); //avoid floating ice chunks on the main path - if (positionType == PositionType.MainPath) + if (positionType == PositionType.MainPath || positionType == PositionType.SidePath) { suitablePositions.RemoveAll(p => ExtraWalls.Any(w => w.Cells.Any(c => c.IsPointInside(p.Position.ToVector2())))); } @@ -1446,14 +2383,23 @@ namespace Barotrauma public void Update(float deltaTime, Camera cam) { LevelObjectManager.Update(deltaTime); - - foreach (LevelWall wall in ExtraWalls) + + foreach (LevelWall wall in ExtraWalls) { wall.Update(deltaTime); } + for (int i = UnsyncedExtraWalls.Count - 1; i >= 0; i--) { - wall.Update(deltaTime); + UnsyncedExtraWalls[i].Update(deltaTime); } - + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { + foreach (LevelWall wall in ExtraWalls) + { + if (wall is DestructibleLevelWall destructibleWall && destructibleWall.NetworkUpdatePending) + { + GameMain.NetworkMember.CreateEntityEvent(this, new object[] { destructibleWall }); + destructibleWall.NetworkUpdatePending = false; + } + } networkUpdateTimer += deltaTime; if (networkUpdateTimer > NetworkUpdateInterval) { @@ -1521,6 +2467,7 @@ namespace Barotrauma foreach (LevelWall wall in ExtraWalls) { + if (wall is DestructibleLevelWall destructibleWall && destructibleWall.Destroyed) { continue; } foreach (VoronoiCell cell in wall.Cells) { tempCells.Add(cell); @@ -1530,6 +2477,68 @@ namespace Barotrauma return tempCells; } + private void CreatePathToClosestTunnel(Point pos) + { + VoronoiCell closestPathCell = null; + double closestDist = 0.0f; + foreach (Tunnel tunnel in Tunnels) + { + if (tunnel.Type == TunnelType.Cave) { continue; } + foreach (VoronoiCell cell in tunnel.Cells) + { + double dist = MathUtils.DistanceSquared(cell.Site.Coord.X, cell.Site.Coord.Y, pos.X, pos.Y); + if (closestPathCell == null || dist < closestDist) + { + closestPathCell = cell; + closestDist = dist; + } + } + } + + //cast a ray from the closest path cell towards the position and remove the cells it hits + List validCells = cells.FindAll(c => c.CellType != CellType.Empty && c.CellType != CellType.Removed); + foreach (VoronoiCell cell in validCells) + { + foreach (GraphEdge e in cell.Edges) + { + if (!MathUtils.LinesIntersect(closestPathCell.Center, pos.ToVector2(), e.Point1, e.Point2)) { continue; } + + cell.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(cell); + } + } + 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) + { + foreach (GraphEdge e2 in cell.Edges) + { + 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++) + { + for (int y = 0; y < cellGrid.GetLength(1); y++) + { + cellGrid[x, y].Remove(adjacentCell); + } + } + cells.Remove(adjacentCell); + break; + } + } + break; + + } + } + } + public string GetWreckIDTag(string originalTag, Submarine wreck) { string shortSeed = ToolBox.StringToInt(LevelData.Seed + wreck?.Info.Name).ToString(); @@ -1537,13 +2546,303 @@ namespace Barotrauma return originalTag + "_" + shortSeed; } + private Submarine SpawnSubOnPath(string subName, ContentFile contentFile, SubmarineType type) + { + var tempSW = new Stopwatch(); + + // Min distance between a sub and the start/end/other sub. + float minDistance = Sonar.DefaultSonarRange; + float squaredMinDistance = minDistance * minDistance; + Vector2 start = startPosition.ToVector2(); + Vector2 end = endPosition.ToVector2(); + var waypoints = WayPoint.WayPointList.Where(wp => + wp.Submarine == null && + wp.SpawnType == SpawnType.Path && + Vector2.DistanceSquared(wp.WorldPosition, start) > squaredMinDistance && + Vector2.DistanceSquared(wp.WorldPosition, end) > squaredMinDistance).ToList(); + + var subDoc = SubmarineInfo.OpenFile(contentFile.Path); + Rectangle subBorders = Submarine.GetBorders(subDoc.Root); + + // Add some margin so that the sub doesn't block the path entirely. It's still possible that some larger subs can't pass by. + Point paddedDimensions = new Point(subBorders.Width + 3000, subBorders.Height + 3000); + + var positions = new List(); + var rects = new List(); + int maxAttempts = 50; + int attemptsLeft = maxAttempts; + bool success = false; + Vector2 spawnPoint = Vector2.Zero; + var allCells = Loaded.GetAllCells(); + while (attemptsLeft > 0) + { + if (attemptsLeft < maxAttempts) + { + Debug.WriteLine($"Failed to position the sub {subName}. Trying again."); + } + attemptsLeft--; + if (TryGetSpawnPoint(out spawnPoint)) + { + success = TryPositionSub(subBorders, subName, ref spawnPoint); + if (success) + { + break; + } + else + { + positions.Clear(); + } + } + else + { + DebugConsole.NewMessage($"Failed to find any spawn point for the sub: {subName} (No valid waypoints left).", Color.Red); + break; + } + } + tempSW.Stop(); + if (success) + { + Debug.WriteLine($"Sub {subName} successfully positioned to {spawnPoint} in {tempSW.ElapsedMilliseconds} (ms)"); + tempSW.Restart(); + SubmarineInfo info = new SubmarineInfo(contentFile.Path) + { + Type = type + }; + Submarine sub = new Submarine(info); + if (type == SubmarineType.Wreck) + { + sub.MakeWreck(); + Wrecks.Add(sub); + PositionsOfInterest.Add(new InterestingPosition(spawnPoint.ToPoint(), PositionType.Wreck, submarine: sub)); + foreach (Hull hull in sub.GetHulls(false)) + { + if (Rand.Value(Rand.RandSync.Server) <= Loaded.GenerationParams.WreckHullFloodingChance) + { + hull.WaterVolume = hull.Volume * Rand.Range(Loaded.GenerationParams.WreckFloodingHullMinWaterPercentage, Loaded.GenerationParams.WreckFloodingHullMaxWaterPercentage, Rand.RandSync.Server); + } + } + // 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 (!sub.CreateWreckAI()) + { + DebugConsole.NewMessage($"Failed to create wreck AI inside {subName}.", Color.Red); + sub.DisableWreckAI(); + } + } + else + { + sub.DisableWreckAI(); + } + } + else if (type == SubmarineType.BeaconStation) + { + sub.ShowSonarMarker = false; + sub.PhysicsBody.FarseerBody.BodyType = BodyType.Static; + sub.TeamID = Character.TeamType.None; + } + tempSW.Stop(); + Debug.WriteLine($"Sub {sub.Info.Name} loaded in { tempSW.ElapsedMilliseconds} (ms)"); + sub.SetPosition(spawnPoint); + wreckPositions.Add(sub, positions); + blockedRects.Add(sub, rects); + + return sub; + } + else + { + DebugConsole.NewMessage($"Failed to position wreck {subName}. Used {tempSW.ElapsedMilliseconds.ToString()} (ms).", Color.Red); + return null; + } + + bool TryPositionSub(Rectangle subBorders, string subName, ref Vector2 spawnPoint) + { + positions.Add(spawnPoint); + bool bottomFound = TryRaycastToBottom(subBorders, ref spawnPoint); + positions.Add(spawnPoint); + + bool leftSideBlocked = IsSideBlocked(subBorders, false); + bool rightSideBlocked = IsSideBlocked(subBorders, true); + int step = 5; + if (rightSideBlocked && !leftSideBlocked) + { + bottomFound = TryMove(subBorders, ref spawnPoint, -step); + } + else if (leftSideBlocked && !rightSideBlocked) + { + bottomFound = TryMove(subBorders, ref spawnPoint, step); + } + else if (!bottomFound) + { + if (!leftSideBlocked) + { + bottomFound = TryMove(subBorders, ref spawnPoint, -step); + } + else if (!rightSideBlocked) + { + bottomFound = TryMove(subBorders, ref spawnPoint, step); + } + else + { + Debug.WriteLine($"Invalid position {spawnPoint}. Does not touch the ground."); + return false; + } + } + positions.Add(spawnPoint); + bool isBlocked = IsBlocked(spawnPoint, subBorders.Size - new Point(step + 50)); + if (isBlocked) + { + rects.Add(ToolBox.GetWorldBounds(spawnPoint.ToPoint(), subBorders.Size)); + Debug.WriteLine($"Invalid position {spawnPoint}. Blocked by level walls."); + } + else if (!bottomFound) + { + Debug.WriteLine($"Invalid position {spawnPoint}. Does not touch the ground."); + } + else + { + var sp = spawnPoint; + if (Wrecks.Any(w => Vector2.DistanceSquared(w.WorldPosition, sp) < squaredMinDistance)) + { + Debug.WriteLine($"Invalid position {spawnPoint}. Too close to other wreck(s)."); + return false; + } + } + return !isBlocked && bottomFound; + + bool TryMove(Rectangle subBorders, ref Vector2 spawnPoint, float amount) + { + float maxMovement = 5000; + float totalAmount = 0; + bool foundBottom = TryRaycastToBottom(subBorders, ref spawnPoint); + while (!IsSideBlocked(subBorders, amount > 0)) + { + foundBottom = TryRaycastToBottom(subBorders, ref spawnPoint); + totalAmount += amount; + spawnPoint = new Vector2(spawnPoint.X + amount, spawnPoint.Y); + if (Math.Abs(totalAmount) > maxMovement) + { + Debug.WriteLine($"Moving the sub {subName} failed."); + break; + } + } + return foundBottom; + } + } + + bool TryGetSpawnPoint(out Vector2 spawnPoint) + { + spawnPoint = Vector2.Zero; + while (waypoints.Any()) + { + var wp = waypoints.GetRandom(Rand.RandSync.Server); + waypoints.Remove(wp); + if (!IsBlocked(wp.WorldPosition, paddedDimensions)) + { + spawnPoint = wp.WorldPosition; + return true; + } + } + return false; + } + + bool TryRaycastToBottom(Rectangle subBorders, ref Vector2 spawnPoint) + { + // Shoot five rays and pick the highest hit point. + int rayCount = 5; + var positions = new Vector2[rayCount]; + bool hit = false; + for (int i = 0; i < rayCount; i++) + { + float quarterWidth = subBorders.Width * 0.25f; + Vector2 rayStart = spawnPoint; + switch (i) + { + case 1: + rayStart = new Vector2(spawnPoint.X - quarterWidth, spawnPoint.Y); + break; + case 2: + rayStart = new Vector2(spawnPoint.X + quarterWidth, spawnPoint.Y); + break; + case 3: + rayStart = new Vector2(spawnPoint.X - quarterWidth / 2, spawnPoint.Y); + break; + case 4: + rayStart = new Vector2(spawnPoint.X + quarterWidth / 2, spawnPoint.Y); + break; + } + var simPos = ConvertUnits.ToSimUnits(rayStart); + var body = Submarine.PickBody(simPos, new Vector2(simPos.X, -1), + customPredicate: f => f.Body?.UserData is VoronoiCell cell && cell.Body.BodyType == BodyType.Static && !ExtraWalls.Any(w => w.Body == f.Body), + collisionCategory: Physics.CollisionLevel | Physics.CollisionWall); + if (body != null) + { + positions[i] = ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition) + new Vector2(0, subBorders.Height / 2); + hit = true; + } + } + float highestPoint = positions.Max(p => p.Y); + spawnPoint = new Vector2(spawnPoint.X, highestPoint); + return hit; + } + + bool IsSideBlocked(Rectangle subBorders, bool front) + { + // Shoot three rays and check whether any of them hits. + int rayCount = 3; + Vector2 halfSize = subBorders.Size.ToVector2() / 2; + Vector2 quarterSize = halfSize / 2; + var positions = new Vector2[rayCount]; + for (int i = 0; i < rayCount; i++) + { + float dir = front ? 1 : -1; + Vector2 rayStart; + Vector2 to; + switch (i) + { + case 1: + rayStart = new Vector2(spawnPoint.X + halfSize.X * dir, spawnPoint.Y + quarterSize.Y); + to = new Vector2(spawnPoint.X + (halfSize.X - quarterSize.X) * dir, rayStart.Y); + break; + case 2: + rayStart = new Vector2(spawnPoint.X + halfSize.X * dir, spawnPoint.Y - quarterSize.Y); + to = new Vector2(spawnPoint.X + (halfSize.X - quarterSize.X) * dir, rayStart.Y); + break; + case 0: + default: + rayStart = spawnPoint; + to = new Vector2(spawnPoint.X + halfSize.X * dir, rayStart.Y); + break; + } + Vector2 simPos = ConvertUnits.ToSimUnits(rayStart); + if (Submarine.PickBody(simPos, ConvertUnits.ToSimUnits(to), + customPredicate: f => f.Body?.UserData is VoronoiCell cell, + collisionCategory: Physics.CollisionLevel | Physics.CollisionWall) != null) + { + return true; + } + } + return false; + } + + bool IsBlocked(Vector2 pos, Point size, float maxDistanceMultiplier = 1) + { + float maxDistance = size.Multiply(maxDistanceMultiplier).ToVector2().LengthSquared(); + Rectangle bounds = ToolBox.GetWorldBounds(pos.ToPoint(), size); + if (Ruins.Any(r => ToolBox.GetWorldBounds(r.Area.Center, r.Area.Size).IntersectsWorld(bounds))) + { + return true; + } + return cells.Any(c => c.Body != null && Vector2.DistanceSquared(pos, c.Center) <= maxDistance && c.BodyVertices.Any(v => bounds.ContainsWorld(v))); + } + } + // For debugging private readonly Dictionary> wreckPositions = new Dictionary>(); private readonly Dictionary> blockedRects = new Dictionary>(); private void CreateWrecks() { var totalSW = new Stopwatch(); - var tempSW = new Stopwatch(); totalSW.Start(); var wreckFiles = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.Wreck).ToList(); if (wreckFiles.None()) @@ -1554,284 +2853,14 @@ namespace Barotrauma wreckFiles.Shuffle(Rand.RandSync.Server); int wreckCount = Math.Min(Loaded.GenerationParams.WreckCount, wreckFiles.Count); - // Min distance between a wreck and the start/end/other wreck. - float minDistance = Sonar.DefaultSonarRange; - float squaredMinDistance = minDistance * minDistance; - Vector2 start = startPosition.ToVector2(); - Vector2 end = endPosition.ToVector2(); - var waypoints = WayPoint.WayPointList.Where(wp => - wp.Submarine == null && - wp.SpawnType == SpawnType.Path && - Vector2.DistanceSquared(wp.WorldPosition, start) > squaredMinDistance && - Vector2.DistanceSquared(wp.WorldPosition, end) > squaredMinDistance).ToList(); Wrecks = new List(wreckCount); for (int i = 0; i < wreckCount; i++) { ContentFile contentFile = wreckFiles[i]; - if (contentFile == null) { continue; } - var subDoc = SubmarineInfo.OpenFile(contentFile.Path); - Rectangle borders = Submarine.GetBorders(subDoc.Root); + if (contentFile == null) { continue; } string wreckName = System.IO.Path.GetFileNameWithoutExtension(contentFile.Path); - // Add some margin so that the wreck doesn't block the path entirely. It's still possible that some larger subs can't pass by. - Point paddedDimensions = new Point(borders.Width + 3000, borders.Height + 3000); - tempSW.Restart(); // For storing the translations. Used only for debugging. - var positions = new List(); - var rects = new List(); - int maxAttempts = 50; - int attemptsLeft = maxAttempts; - bool success = false; - Vector2 spawnPoint = Vector2.Zero; - var allCells = Loaded.GetAllCells(); - while (attemptsLeft > 0) - { - if (attemptsLeft < maxAttempts) - { - Debug.WriteLine($"Failed to position the wreck {wreckName}. Trying again."); - } - attemptsLeft--; - if (TryGetSpawnPoint(out spawnPoint)) - { - success = TryPositionWreck(borders, wreckName, ref spawnPoint); - if (success) - { - break; - } - else - { - positions.Clear(); - } - } - else - { - DebugConsole.NewMessage($"Failed to find any spawn point for the wreck: {wreckName} (No valid waypoints left).", Color.Red); - break; - } - } - tempSW.Stop(); - if (success) - { - Debug.WriteLine($"Wreck {wreckName} successfully positioned to {spawnPoint} in {tempSW.ElapsedMilliseconds} (ms)"); - tempSW.Restart(); - SubmarineInfo info = new SubmarineInfo(contentFile.Path) - { - Type = SubmarineType.Wreck - }; - Submarine wreck = new Submarine(info); - wreck.MakeWreck(); - tempSW.Stop(); - Debug.WriteLine($"Wreck {wreck.Info.Name} loaded in { tempSW.ElapsedMilliseconds} (ms)"); - Wrecks.Add(wreck); - wreck.SetPosition(spawnPoint); - wreckPositions.Add(wreck, positions); - blockedRects.Add(wreck, rects); - PositionsOfInterest.Add(new InterestingPosition(spawnPoint.ToPoint(), PositionType.Wreck, submarine: wreck)); - foreach (Hull hull in wreck.GetHulls(false)) - { - if (Rand.Value(Rand.RandSync.Server) <= Loaded.GenerationParams.WreckHullFloodingChance) - { - hull.WaterVolume = hull.Volume * Rand.Range(Loaded.GenerationParams.WreckFloodingHullMinWaterPercentage, Loaded.GenerationParams.WreckFloodingHullMaxWaterPercentage, Rand.RandSync.Server); - } - } - // Only spawn thalamus when the wreck has some thalamus items defined. - if (Rand.Value(Rand.RandSync.Server) <= Loaded.GenerationParams.ThalamusProbability && wreck.GetItems(false).Any(i => i.Prefab.Category == MapEntityCategory.Thalamus)) - { - if (!wreck.CreateWreckAI()) - { - DebugConsole.NewMessage($"Failed to create wreck AI inside {wreckName}.", Color.Red); - wreck.DisableWreckAI(); - } - } - else - { - wreck.DisableWreckAI(); - } - } - else - { - DebugConsole.NewMessage($"Failed to position wreck {wreckName}. Used {tempSW.ElapsedMilliseconds.ToString()} (ms).", Color.Red); - } - - bool TryPositionWreck(Rectangle borders, string wreckName, ref Vector2 spawnPoint) - { - positions.Add(spawnPoint); - bool bottomFound = TryRaycastToBottom(borders, ref spawnPoint); - positions.Add(spawnPoint); - - bool leftSideBlocked = IsSideBlocked(borders, false); - bool rightSideBlocked = IsSideBlocked(borders, true); - int step = 5; - if (rightSideBlocked && !leftSideBlocked) - { - bottomFound = TryMove(borders, ref spawnPoint, -step); - } - else if (leftSideBlocked && !rightSideBlocked) - { - bottomFound = TryMove(borders, ref spawnPoint, step); - } - else if (!bottomFound) - { - if (!leftSideBlocked) - { - bottomFound = TryMove(borders, ref spawnPoint, -step); - } - else if (!rightSideBlocked) - { - bottomFound = TryMove(borders, ref spawnPoint, step); - } - else - { - Debug.WriteLine($"Invalid position {spawnPoint}. Does not touch the ground."); - return false; - } - } - positions.Add(spawnPoint); - bool isBlocked = IsBlocked(spawnPoint, borders.Size - new Point(step + 50)); - if (isBlocked) - { - rects.Add(ToolBox.GetWorldBounds(spawnPoint.ToPoint(), borders.Size)); - Debug.WriteLine($"Invalid position {spawnPoint}. Blocked by level walls."); - } - else if (!bottomFound) - { - Debug.WriteLine($"Invalid position {spawnPoint}. Does not touch the ground."); - } - else - { - var sp = spawnPoint; - if (Wrecks.Any(w => Vector2.DistanceSquared(w.WorldPosition, sp) < squaredMinDistance)) - { - Debug.WriteLine($"Invalid position {spawnPoint}. Too close to other wreck(s)."); - return false; - } - } - return !isBlocked && bottomFound; - - bool TryMove(Rectangle borders, ref Vector2 spawnPoint, float amount) - { - float maxMovement = 5000; - float totalAmount = 0; - bool foundBottom = TryRaycastToBottom(borders, ref spawnPoint); - while (!IsSideBlocked(borders, amount > 0)) - { - foundBottom = TryRaycastToBottom(borders, ref spawnPoint); - totalAmount += amount; - spawnPoint = new Vector2(spawnPoint.X + amount, spawnPoint.Y); - if (Math.Abs(totalAmount) > maxMovement) - { - Debug.WriteLine($"Moving the wreck {wreckName} failed."); - break; - } - } - return foundBottom; - } - } - - bool TryGetSpawnPoint(out Vector2 spawnPoint) - { - spawnPoint = Vector2.Zero; - while (waypoints.Any()) - { - var wp = waypoints.GetRandom(Rand.RandSync.Server); - waypoints.Remove(wp); - if (!IsBlocked(wp.WorldPosition, paddedDimensions)) - { - spawnPoint = wp.WorldPosition; - return true; - } - } - return false; - } - - static bool TryRaycastToBottom(Rectangle borders, ref Vector2 spawnPoint) - { - // Shoot five rays and pick the highest hit point. - int rayCount = 5; - var positions = new Vector2[rayCount]; - bool hit = false; - for (int i = 0; i < rayCount; i++) - { - float quarterWidth = borders.Width * 0.25f; - Vector2 rayStart = spawnPoint; - switch (i) - { - case 1: - rayStart = new Vector2(spawnPoint.X - quarterWidth, spawnPoint.Y); - break; - case 2: - rayStart = new Vector2(spawnPoint.X + quarterWidth, spawnPoint.Y); - break; - case 3: - rayStart = new Vector2(spawnPoint.X - quarterWidth / 2, spawnPoint.Y); - break; - case 4: - rayStart = new Vector2(spawnPoint.X + quarterWidth / 2, spawnPoint.Y); - break; - } - var simPos = ConvertUnits.ToSimUnits(rayStart); - var body = Submarine.PickBody(simPos, new Vector2(simPos.X, -1), - customPredicate: f => f.Body?.UserData is VoronoiCell cell && cell.Body.BodyType == BodyType.Static, - collisionCategory: Physics.CollisionLevel | Physics.CollisionWall); - if (body != null) - { - positions[i] = ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition) + new Vector2(0, borders.Height / 2); - hit = true; - } - } - float highestPoint = positions.Max(p => p.Y); - spawnPoint = new Vector2(spawnPoint.X, highestPoint); - return hit; - } - - bool IsSideBlocked(Rectangle borders, bool front) - { - // Shoot three rays and check whether any of them hits. - int rayCount = 3; - Vector2 halfSize = borders.Size.ToVector2() / 2; - Vector2 quarterSize = halfSize / 2; - var positions = new Vector2[rayCount]; - for (int i = 0; i < rayCount; i++) - { - float dir = front ? 1 : -1; - Vector2 rayStart; - Vector2 to; - switch (i) - { - case 1: - rayStart = new Vector2(spawnPoint.X + halfSize.X * dir, spawnPoint.Y + quarterSize.Y); - to = new Vector2(spawnPoint.X + (halfSize.X - quarterSize.X) * dir, rayStart.Y); - break; - case 2: - rayStart = new Vector2(spawnPoint.X + halfSize.X * dir, spawnPoint.Y - quarterSize.Y); - to = new Vector2(spawnPoint.X + (halfSize.X - quarterSize.X) * dir, rayStart.Y); - break; - case 0: - default: - rayStart = spawnPoint; - to = new Vector2(spawnPoint.X + halfSize.X * dir, rayStart.Y); - break; - } - Vector2 simPos = ConvertUnits.ToSimUnits(rayStart); - if (Submarine.PickBody(simPos, ConvertUnits.ToSimUnits(to), - customPredicate: f => f.Body?.UserData is VoronoiCell cell, - collisionCategory: Physics.CollisionLevel | Physics.CollisionWall) != null) - { - return true; - } - } - return false; - } - - bool IsBlocked(Vector2 pos, Point size, float maxDistanceMultiplier = 1) - { - float maxDistance = size.Multiply(maxDistanceMultiplier).ToVector2().LengthSquared(); - Rectangle bounds = ToolBox.GetWorldBounds(pos.ToPoint(), size); - if (Ruins.Any(r => ToolBox.GetWorldBounds(r.Area.Center, r.Area.Size).IntersectsWorld(bounds))) - { - return true; - } - return cells.Any(c => c.Body != null && Vector2.DistanceSquared(pos, c.Center) <= maxDistance && c.BodyVertices.Any(v => bounds.ContainsWorld(v))); - } + SpawnSubOnPath(wreckName, contentFile, SubmarineType.Wreck); } totalSW.Stop(); Debug.WriteLine($"{Wrecks.Count} wrecks created in { totalSW.ElapsedMilliseconds.ToString()} (ms)"); @@ -2030,6 +3059,103 @@ namespace Barotrauma } } + private void CreateBeaconStation(List mainPath) + { + if (!LevelData.HasBeaconStation) { return; } + var beaconStationFiles = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.BeaconStation).ToList(); + if (beaconStationFiles.None()) + { + DebugConsole.ThrowError("No BeaconStation files found in the selected content packages!"); + return; + } + var contentFile = beaconStationFiles.GetRandom(Rand.RandSync.Server); + 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(); + } + + public void PrepareBeaconStation() + { + if (!LevelData.HasBeaconStation) { return; } + if (GameMain.NetworkMember?.IsClient ?? false) { return; } + + List beaconItems = Item.ItemList.FindAll(it => it.Submarine == BeaconStation); + + Item reactorItem = beaconItems.Find(it => it.GetComponent() != null); + Reactor reactorComponent = reactorItem.GetComponent(); + ItemContainer reactorContainer = reactorItem.GetComponent(); + + if (LevelData.IsBeaconActive) + { + if (reactorContainer.Inventory.IsEmpty()) + { + ItemPrefab fuelPrefab = ItemPrefab.Prefabs[reactorContainer.ContainableItems[0].Identifiers[0]]; + Entity.Spawner.AddToSpawnQueue( + fuelPrefab, reactorContainer.Inventory, + onSpawned: (it) => reactorComponent.PowerUpImmediately()); + } + beaconSonar.CurrentMode = Sonar.Mode.Active; +#if SERVER + beaconSonar.Item.CreateServerEvent(beaconSonar); +#endif + } + else + { + if (!(GameMain.NetworkMember?.IsClient ?? false)) + { + //empty the reactor + foreach (Item item in reactorContainer.Inventory.Items) + { + if (item == null) { continue; } + Entity.Spawner.AddToRemoveQueue(item); + } + + //remove wires + foreach (Item item in beaconItems.Where(it => it.GetComponent() != null).ToList()) + { + if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < 0.25f) + { + Entity.Spawner.AddToRemoveQueue(item); + } + } + + //break powered items + foreach (Item item in beaconItems.Where(it => it.Components.Any(c => c is Powered))) + { + if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < 0.5f) + { + item.Condition *= Rand.Range(0.2f, 0.6f, Rand.RandSync.Unsynced); + } + } + + //poke holes in the walls + foreach (Structure structure in Structure.WallList.Where(s => s.Submarine == BeaconStation)) + { + if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < 0.25f) + { + int sectionIndex = Rand.Range(0, structure.SectionCount - 1, Rand.RandSync.Unsynced); + structure.AddDamage(sectionIndex, Rand.Range(structure.MaxHealth * 0.2f, structure.MaxHealth, Rand.RandSync.Unsynced)); + } + } + } + } + } + + public bool CheckBeaconActive() + { + if (beaconSonar == null) { return false; } + return beaconSonar.Voltage > beaconSonar.MinVoltage && beaconSonar.CurrentMode == Sonar.Mode.Active; + } + private bool IsModeStartOutpostCompatible() { #if CLIENT @@ -2135,6 +3261,14 @@ namespace Barotrauma } } + /// + /// Calculate the "real" depth in meters from the surface of Europa + /// + public float GetRealWorldDepth(float worldPositionY) + { + return (-(worldPositionY - GenerationParams.Height) + LevelData.InitialDepth) * Physics.DisplayToRealWorldRatio; + } + public void DebugSetStartLocation(Location newStartLocation) { StartLocation = newStartLocation; @@ -2170,13 +3304,14 @@ namespace Barotrauma if (ExtraWalls != null) { - foreach (LevelWall w in ExtraWalls) - { - w.Dispose(); - } - + foreach (LevelWall w in ExtraWalls) { w.Dispose(); } ExtraWalls = null; } + if (UnsyncedExtraWalls != null) + { + foreach (LevelWall w in UnsyncedExtraWalls) { w.Dispose(); } + UnsyncedExtraWalls = null; + } cells = null; @@ -2188,17 +3323,28 @@ namespace Barotrauma Loaded = null; } - + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { - foreach (LevelWall levelWall in ExtraWalls) + if (extraData != null && extraData.Length > 0 && extraData[0] is DestructibleLevelWall destructibleWall) { - if (levelWall.Body.BodyType == BodyType.Static) continue; - - msg.Write(levelWall.Body.Position.X); - msg.Write(levelWall.Body.Position.Y); - msg.WriteRangedSingle(levelWall.MoveState, 0.0f, MathHelper.TwoPi, 16); + int index = ExtraWalls.IndexOf(destructibleWall); + msg.Write(false); + msg.Write((ushort)(index == -1 ? ushort.MaxValue : index)); + //write health using one byte + msg.Write((byte)MathHelper.Clamp((int)(MathUtils.InverseLerp(0.0f, destructibleWall.MaxHealth, destructibleWall.Damage) * 255.0f), 0, 255)); + } + else + { + msg.Write(true); + foreach (LevelWall levelWall in ExtraWalls) + { + if (levelWall.Body.BodyType == BodyType.Static) { continue; } + msg.Write(levelWall.Body.Position.X); + msg.Write(levelWall.Body.Position.Y); + msg.WriteRangedSingle(levelWall.MoveState, 0.0f, MathHelper.TwoPi, 16); + } } } - } + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs index 62f44a084..ac0896f4e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelData.cs @@ -26,13 +26,33 @@ namespace Barotrauma public readonly LevelGenerationParams GenerationParams; + public bool HasBeaconStation; + public bool IsBeaconActive; + public OutpostGenerationParams ForceOutpostGenerationParams; public readonly Point Size; + public readonly int InitialDepth; + public readonly List EventHistory = new List(); public readonly List NonRepeatableEvents = new List(); + public float CrushDepth + { + get + { + return Math.Max(Size.Y, Level.DefaultRealWorldCrushDepth / Physics.DisplayToRealWorldRatio) - InitialDepth; + } + } + public float RealWorldCrushDepth + { + get + { + return Math.Max(Size.Y * Physics.DisplayToRealWorldRatio, Level.DefaultRealWorldCrushDepth); + } + } + public LevelData(string seed, float difficulty, float sizeFactor, LevelGenerationParams generationParams, Biome biome) { Seed = seed ?? throw new ArgumentException("Seed was null"); @@ -44,6 +64,8 @@ namespace Barotrauma sizeFactor = MathHelper.Clamp(sizeFactor, 0.0f, 1.0f); int width = (int)MathHelper.Lerp(generationParams.MinWidth, generationParams.MaxWidth, sizeFactor); + InitialDepth = (int)MathHelper.Lerp(generationParams.InitialDepthMin, generationParams.InitialDepthMax, sizeFactor); + Size = new Point( (int)MathUtils.Round(width, Level.GridCellSize), (int)MathUtils.Round(generationParams.Height, Level.GridCellSize)); @@ -56,8 +78,11 @@ namespace Barotrauma Size = element.GetAttributePoint("size", new Point(1000)); Enum.TryParse(element.GetAttributeString("type", "LocationConnection"), out Type); + HasBeaconStation = element.GetAttributeBool("hasbeaconstation", false); + IsBeaconActive = element.GetAttributeBool("isbeaconactive", false); + string generationParamsId = element.GetAttributeString("generationparams", ""); - GenerationParams = LevelGenerationParams.LevelParams.Find(l => l.Identifier == generationParamsId); + GenerationParams = LevelGenerationParams.LevelParams.Find(l => l.Identifier == generationParamsId || l.OldIdentifier == generationParamsId); if (GenerationParams == null) { DebugConsole.ThrowError($"Error while loading a level. Could not find level generation params with the ID \"{generationParamsId}\"."); @@ -68,8 +93,10 @@ 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}\"."); @@ -104,6 +131,12 @@ namespace Barotrauma Size = new Point( (int)MathUtils.Round(width, Level.GridCellSize), (int)MathUtils.Round(GenerationParams.Height, Level.GridCellSize)); + + 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(); + IsBeaconActive = false; } /// @@ -119,6 +152,7 @@ namespace Barotrauma var rand = new MTRandom(ToolBox.StringToInt(Seed)); int width = (int)MathHelper.Lerp(GenerationParams.MinWidth, GenerationParams.MaxWidth, (float)rand.NextDouble()); + InitialDepth = (int)MathHelper.Lerp(GenerationParams.InitialDepthMin, GenerationParams.InitialDepthMax, (float)rand.NextDouble()); Size = new Point( (int)MathUtils.Round(width, Level.GridCellSize), (int)MathUtils.Round(GenerationParams.Height, Level.GridCellSize)); @@ -136,16 +170,24 @@ namespace Barotrauma LevelType type = generationParams == null ? LevelData.LevelType.LocationConnection : generationParams.Type; if (generationParams == null) { generationParams = LevelGenerationParams.GetRandom(seed, type); } - var biome = - LevelGenerationParams.GetBiomes().FirstOrDefault(b => generationParams.AllowedBiomes.Contains(b)) ?? + var biome = + LevelGenerationParams.GetBiomes().FirstOrDefault(b => generationParams.AllowedBiomes.Contains(b)) ?? LevelGenerationParams.GetBiomes().GetRandom(Rand.RandSync.Server); - return new LevelData( + 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); + if (type == LevelType.LocationConnection) + { + float beaconRng = Rand.Range(0.0f, 1.0f, Rand.RandSync.Server); + levelData.HasBeaconStation = beaconRng < 0.5f; + levelData.IsBeaconActive = beaconRng > 0.25f; + } + GameMain.GameSession?.GameMode?.Mission?.AdjustLevelData(levelData); + return levelData; } public void Save(XElement parentElement) @@ -156,7 +198,15 @@ namespace Barotrauma new XAttribute("type", Type.ToString()), new XAttribute("difficulty", Difficulty.ToString("G", CultureInfo.InvariantCulture)), new XAttribute("size", XMLExtensions.PointToString(Size)), - new XAttribute("generationparams", GenerationParams.Identifier)); + new XAttribute("generationparams", GenerationParams.Identifier), + new XAttribute("initialdepth", InitialDepth)); + + if (HasBeaconStation) + { + newElement.Add( + new XAttribute("hasbeaconstation", HasBeaconStation.ToString()), + new XAttribute("isbeaconactive", IsBeaconActive.ToString())); + } if (Type == LevelType.Outpost) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs index 549f78940..0ce80828c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs @@ -1,5 +1,4 @@ -using Barotrauma.Extensions; -using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; @@ -10,6 +9,7 @@ namespace Barotrauma class Biome { public readonly string Identifier; + public readonly string OldIdentifier; public readonly string DisplayName; public readonly string Description; @@ -26,6 +26,7 @@ namespace Barotrauma public Biome(XElement element) { Identifier = element.GetAttributeString("identifier", ""); + OldIdentifier = element.GetAttributeString("oldidentifier", null); if (string.IsNullOrEmpty(Identifier)) { Identifier = element.GetAttributeString("name", ""); @@ -61,6 +62,8 @@ namespace Barotrauma public readonly string Identifier; + public readonly string OldIdentifier; + private int minWidth, maxWidth, height; private Point voronoiSiteInterval; @@ -72,9 +75,7 @@ namespace Barotrauma //x = min interval, y = max interval private Point mainPathNodeIntervalRange; - private int smallTunnelCount; - //x = min length, y = max length - private Point smallTunnelLengthRange; + private int caveCount; //how large portion of the bottom of the level should be "carved out" //if 0.0f, the bottom will be completely solid (making the abyss unreachable) @@ -96,6 +97,8 @@ namespace Barotrauma private float waterParticleScale; + private int initialDepthMin, initialDepthMax; + //which biomes can this type of level appear in private readonly List allowedBiomes = new List(); @@ -146,7 +149,7 @@ namespace Barotrauma } private Vector2 startPosition; - [Serialize("0,0", true, "Start position of the level (relative to the size of the level. 0,0 = top left corner, 1,1 = bottom right corner)"), Editable] + [Serialize("0,0", true, "Start position of the level (relative to the size of the level. 0,0 = top left corner, 1,1 = bottom right corner)"), Editable(DecimalCount = 2)] public Vector2 StartPosition { get { return startPosition; } @@ -159,7 +162,7 @@ namespace Barotrauma } private Vector2 endPosition; - [Serialize("1,0", true, "End position of the level (relative to the size of the level. 0,0 = top left corner, 1,1 = bottom right corner)"), Editable] + [Serialize("1,0", true, "End position of the level (relative to the size of the level. 0,0 = top left corner, 1,1 = bottom right corner)"), Editable(DecimalCount = 2)] public Vector2 EndPosition { get { return endPosition; } @@ -185,25 +188,46 @@ namespace Barotrauma set; } - [Serialize(100000, true), Editable(MinValueInt = 10000, MaxValueInt = 1000000)] + [Serialize(80, true, description: "The total number of decorative background creatures."), Editable(MinValueInt = 0, MaxValueInt = 1000)] + public int BackgroundCreatureAmount + { + get; + set; + } + + [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)] + public int InitialDepthMin + { + get { return initialDepthMin; } + set { initialDepthMin = Math.Max(value, 0); } + } + + [Serialize(80000, true), Editable(MinValueInt = 0, MaxValueInt = 1000000)] + public int InitialDepthMax + { + get { return initialDepthMax; } + set { initialDepthMax = Math.Max(value, initialDepthMin); } } [Serialize(6500, true), Editable(MinValueInt = 5000, MaxValueInt = 1000000)] @@ -213,6 +237,29 @@ namespace Barotrauma set; } + + [Serialize("0,1", true), Editable] + public Point SideTunnelCount + { + get; + set; + } + + + [Serialize(0.5f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] + public float SideTunnelVariance + { + get; + set; + } + + [Serialize("2000,6000", true), Editable] + public Point MinSideTunnelRadius + { + get; + set; + } + [Editable, Serialize("3000, 3000", true, description: "How far from each other voronoi sites are placed. " + "Sites determine shape of the voronoi graph which the level walls are generated from. " + "(Decreasing this value causes the number of sites, and the complexity of the level, to increase exponentially - be careful when adjusting)")] @@ -239,7 +286,7 @@ namespace Barotrauma } } - [Editable(MinValueInt = 100, MaxValueInt = 10000), Serialize(1000, true, description: "The edges of the individual wall cells are subdivided into edges of this size. " + [Editable(MinValueInt = 500, MaxValueInt = 10000), Serialize(5000, true, description: "The edges of the individual wall cells are subdivided into edges of this size. " + "Can be used in conjunction with the rounding values to make the cells rounder. Smaller values will make the cells look smoother, " + "but make the level more performance-intensive as the number of polygons used in rendering and physics calculations increases.")] public int CellSubdivisionLength @@ -287,23 +334,18 @@ namespace Barotrauma } } - [Editable, Serialize(5, true, description: "The number of small tunnels placed along the main path.")] - public int SmallTunnelCount + [Serialize(0.5f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)] + public float MainPathVariance { - get { return smallTunnelCount; } - set { smallTunnelCount = MathHelper.Clamp(value, 0, 100); } + get; + set; } - [Editable(VectorComponentLabels = new string[] { "editable.minvalue", "editable.maxvalue" }), - Serialize("5000, 10000", true, description: "The minimum and maximum length of small tunnels placed along the main path.")] - public Point SmallTunnelLengthRange + [Editable, Serialize(5, true, description: "The number of caves placed along the main path.")] + public int CaveCount { - get { return smallTunnelLengthRange; } - set - { - smallTunnelLengthRange.X = MathHelper.Clamp(value.X, 100, MinWidth); - smallTunnelLengthRange.Y = MathHelper.Clamp(value.Y, smallTunnelLengthRange.X, MinWidth); - } + get { return caveCount; } + set { caveCount = MathHelper.Clamp(value, 0, 100); } } [Serialize(100, true), Editable(MinValueInt = 0, MaxValueInt = 10000)] @@ -313,6 +355,34 @@ namespace Barotrauma set; } + [Serialize("19200,38400", true, description: "The minimum and maximum distance between two resource spawn points on a path."), Editable(100, 100000)] + public Point ResourceIntervalRange + { + get; + set; + } + + [Serialize("9600,19200", true, description: "The minimum and maximum distance between two resource spawn points on a cave path."), Editable(100, 100000)] + public Point CaveResourceIntervalRange + { + get; + set; + } + + [Serialize("2,8", true, description: "The minimum and maximum amount of resources in a single cluster. " + + "In addition to this, resource commonness affects the cluster size. Less common resources spawn in smaller clusters."), Editable(1, 20)] + public Point ResourceClusterSizeRange + { + get; + set; + } + + [Serialize(0.3f, true, description: "How likely a resource spawn point on a path is to contain resources."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] + public float ResourceSpawnChance { get; set; } + + [Serialize(1.0f, true, description: "How likely a resource spawn point on a cave path is to contain resources."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] + public float CaveResourceSpawnChance { get; set; } + [Serialize(0, true), Editable(MinValueInt = 0, MaxValueInt = 20)] public int FloatingIceChunkCount { @@ -320,6 +390,20 @@ namespace Barotrauma set; } + [Serialize(0, true), Editable(MinValueInt = 0, MaxValueInt = 100)] + public int IslandCount + { + get; + set; + } + + [Serialize(0, true), Editable(MinValueInt = 0, MaxValueInt = 20)] + public int IceSpireCount + { + 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 { @@ -415,12 +499,41 @@ namespace Barotrauma private set { waterParticleScale = Math.Max(value, 0.01f); } } + [Serialize(2048.0f, true, description: "Size of the level wall texture."), Editable(minValue: 10.0f, maxValue: 10000.0f)] + public float WallTextureSize + { + get; + private set; + } + + [Serialize(2048.0f, true), Editable(minValue: 10.0f, maxValue: 10000.0f)] + public float WallEdgeTextureWidth + { + get; + private set; + } + + [Serialize(120.0f, true, description: "How far the level walls' edge texture portrudes outside the actual, \"physical\" edge of the cell."), Editable(minValue: 0.0f, maxValue: 1000.0f)] + public float WallEdgeExpandOutwardsAmount + { + get; + private set; + } + + [Serialize(1000.0f, true, description: "How far inside the level walls the edge texture continues."), Editable(minValue: 0.0f, maxValue: 10000.0f)] + public float WallEdgeExpandInwardsAmount + { + get; + private set; + } + public Sprite BackgroundSprite { get; private set; } public Sprite BackgroundTopSprite { get; private set; } public Sprite WallSprite { get; private set; } - public Sprite WallSpriteSpecular { get; private set; } public Sprite WallEdgeSprite { get; private set; } - public Sprite WallEdgeSpriteSpecular { get; private set; } + public Sprite DestructibleWallSprite { get; private set; } + public Sprite DestructibleWallEdgeSprite { get; private set; } + public Sprite WallSpriteDestroyed { get; private set; } public Sprite WaterParticles { get; private set; } public static IEnumerable GetBiomes() @@ -469,6 +582,8 @@ namespace Barotrauma { Identifier = element == null ? "default" : element.GetAttributeString("identifier", null) ?? element.Name.ToString(); + OldIdentifier = element?.GetAttributeString("oldidentifier", null)?.ToLowerInvariant(); + Identifier = Identifier.ToLowerInvariant(); SerializableProperties = SerializableProperty.DeserializeProperties(this, element); if (element == null) { return; } @@ -486,7 +601,8 @@ namespace Barotrauma string biomeName = biomeNames[i].Trim().ToLowerInvariant(); if (biomeName == "none") { continue; } - Biome matchingBiome = biomes.Find(b => b.Identifier.Equals(biomeName, StringComparison.OrdinalIgnoreCase)); + Biome matchingBiome = biomes.Find(b => + b.Identifier.Equals(biomeName, StringComparison.OrdinalIgnoreCase) || (b.OldIdentifier?.Equals(biomeName, StringComparison.OrdinalIgnoreCase) ?? false)); if (matchingBiome == null) { matchingBiome = biomes.Find(b => b.DisplayName.Equals(biomeName, StringComparison.OrdinalIgnoreCase)); @@ -518,14 +634,17 @@ namespace Barotrauma case "wall": WallSprite = new Sprite(subElement); break; - case "wallspecular": - WallSpriteSpecular = new Sprite(subElement); - break; case "walledge": WallEdgeSprite = new Sprite(subElement); break; - case "walledgespecular": - WallEdgeSpriteSpecular = new Sprite(subElement); + case "destructiblewall": + DestructibleWallSprite = new Sprite(subElement); + break; + case "destructiblewalledge": + DestructibleWallEdgeSprite = new Sprite(subElement); + break; + case "walldestroyed": + WallSpriteDestroyed = new Sprite(subElement); break; case "waterparticles": WaterParticles = new Sprite(subElement); @@ -546,8 +665,7 @@ namespace Barotrauma } List biomeElements = new List(); - List levelParamElements = new List(); - + Dictionary levelParamElements = new Dictionary(); foreach (ContentFile file in files) { XDocument doc = XMLExtensions.TryLoadXml(file.Path); @@ -557,18 +675,18 @@ namespace Barotrauma { mainElement = doc.Root.FirstElement(); biomeElements.Clear(); - levelParamElements.Clear(); - DebugConsole.NewMessage($"Overriding the level generation parameters and biomes with '{file.Path}'", Color.Yellow); + DebugConsole.NewMessage($"Overriding biomes with '{file.Path}'", Color.Yellow); } - else if (biomeElements.Any() || levelParamElements.Any()) + else if (biomeElements.Any() && mainElement.Name.ToString().Equals("biomes", StringComparison.OrdinalIgnoreCase)) { - DebugConsole.ThrowError($"Error in '{file.Path}': Another level generation parameter file already loaded! Use tags to override it."); + DebugConsole.ThrowError($"Error in '{file.Path}': Another level generation parameter file already loaded! Use tags to override the biomes."); break; } foreach (XElement element in mainElement.Elements()) { - if (element.IsOverride()) + bool isOverride = element.IsOverride(); + if (isOverride) { if (element.FirstElement().Name.ToString().Equals("biomes", StringComparison.OrdinalIgnoreCase)) { @@ -578,18 +696,31 @@ namespace Barotrauma } else { - levelParamElements.Clear(); - DebugConsole.NewMessage($"Overriding the level generation parameters with '{file.Path}'", Color.Yellow); - levelParamElements.AddRange(element.Elements()); + string identifier = element.FirstElement().GetAttributeString("identifier", null) ?? element.GetAttributeString("name", ""); + if (levelParamElements.ContainsKey(identifier)) + { + DebugConsole.NewMessage($"Overriding the level generation parameters '{identifier}' using the file '{file.Path}'", Color.Yellow); + levelParamElements.Remove(identifier); + } + levelParamElements.Add(identifier, element.FirstElement()); } - } + } else if (element.Name.ToString().Equals("biomes", StringComparison.OrdinalIgnoreCase)) { biomeElements.AddRange(element.Elements()); } else { - levelParamElements.Add(element); + string identifier = element.GetAttributeString("identifier", null) ?? element.GetAttributeString("name", ""); + if (levelParamElements.ContainsKey(identifier)) + { + DebugConsole.ThrowError($"Duplicate level generation parameters: '{identifier}' defined in {element.Name} of '{file.Path}'. Use tags to override the generation parameters."); + continue; + } + else + { + levelParamElements.Add(identifier, element); + } } } } @@ -599,7 +730,7 @@ namespace Barotrauma biomes.Add(new Biome(biomeElement)); } - foreach (XElement levelParamElement in levelParamElements) + foreach (XElement levelParamElement in levelParamElements.Values) { LevelParams.Add(new LevelGenerationParams(levelParamElement)); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObject.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObject.cs index 3bf90901c..510d21414 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObject.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObject.cs @@ -37,18 +37,23 @@ namespace Barotrauma public bool NeedsNetworkSyncing { - get { return Triggers.Any(t => t.NeedsNetworkSyncing); } - set { Triggers.ForEach(t => t.NeedsNetworkSyncing = false); } + get { return Triggers != null && Triggers.Any(t => t.NeedsNetworkSyncing); } + set + { + if (Triggers == null) { return; } + Triggers.ForEach(t => t.NeedsNetworkSyncing = false); + } + } + + public bool NeedsUpdate + { + get; private set; } public Sprite Sprite { get { return spriteIndex < 0 || Prefab.Sprites.Count == 0 ? null : Prefab.Sprites[spriteIndex % Prefab.Sprites.Count]; } } - public Sprite SpecularSprite - { - get { return spriteIndex < 0 || Prefab.SpecularSprites.Count == 0 ? null : Prefab.SpecularSprites[spriteIndex % Prefab.SpecularSprites.Count]; } - } Vector2 ISpatialEntity.Position => new Vector2(Position.X, Position.Y); @@ -60,8 +65,6 @@ namespace Barotrauma public LevelObject(LevelObjectPrefab prefab, Vector3 position, float scale, float rotation = 0.0f) { - Triggers = new List(); - ActivePrefab = Prefab = prefab; Position = position; Scale = scale; @@ -69,13 +72,26 @@ namespace Barotrauma spriteIndex = ActivePrefab.Sprites.Any() ? Rand.Int(ActivePrefab.Sprites.Count, Rand.RandSync.Server) : -1; - if (prefab.PhysicsBodyElement != null) + if (Sprite != null && prefab.SpriteSpecificPhysicsBodyElements.ContainsKey(Sprite)) + { + PhysicsBody = new PhysicsBody(prefab.SpriteSpecificPhysicsBodyElements[Sprite], ConvertUnits.ToSimUnits(new Vector2(position.X, position.Y)), Scale); + } + else if (prefab.PhysicsBodyElement != null) { PhysicsBody = new PhysicsBody(prefab.PhysicsBodyElement, ConvertUnits.ToSimUnits(new Vector2(position.X, position.Y)), Scale); } + if (PhysicsBody != null) + { + PhysicsBody.SetTransformIgnoreContacts(PhysicsBody.SimPosition, -Rotation); + PhysicsBody.BodyType = BodyType.Static; + PhysicsBody.CollisionCategories = Physics.CollisionLevel; + PhysicsBody.CollidesWith = Physics.CollisionWall | Physics.CollisionCharacter; + } + foreach (XElement triggerElement in prefab.LevelTriggerElements) { + Triggers ??= new List(); Vector2 triggerPosition = triggerElement.GetAttributeVector2("position", Vector2.Zero) * scale; if (rotation != 0.0f) @@ -90,10 +106,12 @@ namespace Barotrauma var newTrigger = new LevelTrigger(triggerElement, new Vector2(position.X, position.Y) + triggerPosition, -rotation, scale, prefab.Name); int parentTriggerIndex = prefab.LevelTriggerElements.IndexOf(triggerElement.Parent); - if (parentTriggerIndex > -1) newTrigger.ParentTrigger = Triggers[parentTriggerIndex]; + if (parentTriggerIndex > -1) { newTrigger.ParentTrigger = Triggers[parentTriggerIndex]; } Triggers.Add(newTrigger); } + NeedsUpdate = NeedsNetworkSyncing || (Triggers != null && Triggers.Any()) || Prefab.PhysicsBodyTriggerIndex > -1; + InitProjSpecific(); } @@ -131,9 +149,10 @@ namespace Barotrauma public void ServerWrite(IWriteMessage msg, Client c) { + if (Triggers == null) { return; } for (int j = 0; j < Triggers.Count; j++) { - if (!Triggers[j].UseNetworkSyncing) continue; + if (!Triggers[j].UseNetworkSyncing) { continue; } Triggers[j].ServerWrite(msg, c); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs index d37556911..4790612b9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.Linq; using System.Xml.Linq; using Voronoi2; +using Barotrauma.Extensions; namespace Barotrauma { @@ -17,9 +18,10 @@ namespace Barotrauma const int GridSize = 2000; private List objects; + private List updateableObjects; private List[,] objectGrid; - public LevelObjectManager() : base(null) + public LevelObjectManager() : base(null, Entity.NullEntityID) { } @@ -27,20 +29,36 @@ namespace Barotrauma { public readonly GraphEdge GraphEdge; public readonly Vector2 Normal; - public readonly LevelObjectPrefab.SpawnPosType SpawnPosType; + public readonly List SpawnPosTypes = new List(); public readonly Alignment Alignment; public readonly float Length; private readonly float noiseVal; + public SpawnPosition(GraphEdge graphEdge, Vector2 normal, LevelObjectPrefab.SpawnPosType spawnPosType, Alignment alignment) + : this(graphEdge, normal, spawnPosType.ToEnumerable(), alignment) + { } + + public SpawnPosition(GraphEdge graphEdge, Vector2 normal, IEnumerable spawnPosTypes, Alignment alignment) { GraphEdge = graphEdge; - Normal = normal; - SpawnPosType = spawnPosType; - Alignment = alignment; - - Length = Vector2.Distance(graphEdge.Point1, graphEdge.Point2); + Normal = normal.NearlyEquals(Vector2.Zero) ? Vector2.UnitY : Vector2.Normalize(normal); + SpawnPosTypes.AddRange(spawnPosTypes); + + if (spawnPosTypes.Contains(LevelObjectPrefab.SpawnPosType.MainPath) || + spawnPosTypes.Contains(LevelObjectPrefab.SpawnPosType.LevelStart) || + spawnPosTypes.Contains(LevelObjectPrefab.SpawnPosType.LevelEnd)) + { + Length = 1000.0f; + Normal = Vector2.Zero; + Alignment = Alignment.Any; + } + else + { + Alignment = alignment; + Length = Vector2.Distance(graphEdge.Point1, graphEdge.Point2); + } noiseVal = (float)(PerlinNoise.CalculatePerlin(GraphEdge.Point1.X / 10000.0f, GraphEdge.Point1.Y / 10000.0f, 0.5f) + @@ -83,7 +101,7 @@ namespace Barotrauma foreach (var posOfInterest in level.PositionsOfInterest) { - if (posOfInterest.PositionType != Level.PositionType.MainPath) continue; + if (posOfInterest.PositionType != Level.PositionType.MainPath && posOfInterest.PositionType != Level.PositionType.SidePath) { continue; } availableSpawnPositions.Add(new SpawnPosition( new GraphEdge(posOfInterest.Position.ToVector2(), posOfInterest.Position.ToVector2() + Vector2.UnitX), @@ -101,49 +119,29 @@ namespace Barotrauma var availablePrefabs = new List(LevelObjectPrefab.List); objects = new List(); + updateableObjects = new List(); Dictionary> suitableSpawnPositions = new Dictionary>(); Dictionary> spawnPositionWeights = new Dictionary>(); for (int i = 0; i < amount; i++) { //get a random prefab and find a place to spawn it - LevelObjectPrefab prefab = GetRandomPrefab(level.GenerationParams.Identifier, availablePrefabs); + LevelObjectPrefab prefab = GetRandomPrefab(level.GenerationParams, availablePrefabs); if (prefab == null) { continue; } if (!suitableSpawnPositions.ContainsKey(prefab)) { - suitableSpawnPositions.Add(prefab, availableSpawnPositions.Where(sp => - prefab.SpawnPos.HasFlag(sp.SpawnPosType) && (sp.Length >= prefab.MinSurfaceWidth && prefab.Alignment.HasFlag(sp.Alignment) || sp.SpawnPosType == LevelObjectPrefab.SpawnPosType.LevelEnd || sp.SpawnPosType == LevelObjectPrefab.SpawnPosType.LevelStart)).ToList()); + suitableSpawnPositions.Add(prefab, + availableSpawnPositions.Where(sp => + sp.SpawnPosTypes.Any(type => prefab.SpawnPos.HasFlag(type)) && + 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; } - - float rotation = 0.0f; - if (prefab.AlignWithSurface && spawnPosition != null) - { - rotation = MathUtils.VectorToAngle(new Vector2(spawnPosition.Normal.Y, spawnPosition.Normal.X)); - } - rotation += Rand.Range(prefab.RandomRotationRad.X, prefab.RandomRotationRad.Y, Rand.RandSync.Server); - - Vector2 position = Vector2.Zero; - Vector2 edgeDir = Vector2.UnitX; - if (spawnPosition == null) - { - position = new Vector2( - Rand.Range(0.0f, level.Size.X, Rand.RandSync.Server), - Rand.Range(0.0f, level.Size.Y, Rand.RandSync.Server)); - } - else - { - edgeDir = (spawnPosition.GraphEdge.Point1 - spawnPosition.GraphEdge.Point2) / spawnPosition.Length; - position = spawnPosition.GraphEdge.Point2 + edgeDir * Rand.Range(prefab.MinSurfaceWidth / 2.0f, spawnPosition.Length - prefab.MinSurfaceWidth / 2.0f, Rand.RandSync.Server); - } - - 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); + PlaceObject(prefab, spawnPosition, level); if (prefab.MaxCount < amount) { if (objects.Count(o => o.Prefab == prefab) >= prefab.MaxCount) @@ -151,38 +149,119 @@ namespace Barotrauma availablePrefabs.Remove(prefab); } } + } - foreach (LevelObjectPrefab.ChildObject child in prefab.ChildObjects) + 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(); + + var caveCells = cave.Tunnels.SelectMany(t => t.Cells); + List caveWallCells = new List(); + foreach (var edge in caveCells.SelectMany(c => c.Edges)) { - int childCount = Rand.Range(child.MinCount, child.MaxCount, Rand.RandSync.Server); - for (int j = 0; j < childCount; j++) - { - 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; - - Vector2 childPos = position + edgeDir * Rand.Range(-0.5f, 0.5f, Rand.RandSync.Server) * prefab.MinSurfaceWidth; - - var childObject = new LevelObject(childPrefab, - new Vector3(childPos, Rand.Range(childPrefab.DepthRange.X, childPrefab.DepthRange.Y, Rand.RandSync.Server)), - Rand.Range(childPrefab.MinSize, childPrefab.MaxSize, Rand.RandSync.Server), - rotation + Rand.Range(childPrefab.RandomRotationRad.X, childPrefab.RandomRotationRad.Y, Rand.RandSync.Server)); - - AddObject(childObject, level); - } + if (!edge.NextToCave) { 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 < cave.CaveGenerationParams.LevelObjectAmount; i++) + { + //get a random prefab and find a place to spawn it + LevelObjectPrefab prefab = GetRandomPrefab(cave.CaveGenerationParams, availablePrefabs); + 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 (prefab.MaxCount < amount) + { + if (objects.Count(o => o.Prefab == prefab) >= prefab.MaxCount) + { + availablePrefabs.Remove(prefab); + } + } + } + } + } + + private void PlaceObject(LevelObjectPrefab prefab, SpawnPosition spawnPosition, Level level) + { + float rotation = 0.0f; + if (prefab.AlignWithSurface && spawnPosition.Normal.LengthSquared() > 0.001f && spawnPosition != null) + { + rotation = MathUtils.VectorToAngle(new Vector2(spawnPosition.Normal.Y, spawnPosition.Normal.X)); + } + rotation += Rand.Range(prefab.RandomRotationRad.X, prefab.RandomRotationRad.Y, Rand.RandSync.Server); + + Vector2 position = Vector2.Zero; + Vector2 edgeDir = Vector2.UnitX; + if (spawnPosition == null) + { + position = new Vector2( + Rand.Range(0.0f, level.Size.X, Rand.RandSync.Server), + Rand.Range(0.0f, level.Size.Y, Rand.RandSync.Server)); + } + else + { + edgeDir = (spawnPosition.GraphEdge.Point1 - spawnPosition.GraphEdge.Point2) / spawnPosition.Length; + position = spawnPosition.GraphEdge.Point2 + edgeDir * Rand.Range(prefab.MinSurfaceWidth / 2.0f, spawnPosition.Length - prefab.MinSurfaceWidth / 2.0f, Rand.RandSync.Server); + } + + if (!MathUtils.NearlyEqual(prefab.RandomOffset.X, 0.0f) || !MathUtils.NearlyEqual(prefab.RandomOffset.Y, 0.0f)) + { + Vector2 offsetDir = spawnPosition.Normal.LengthSquared() > 0.001f ? spawnPosition.Normal : Rand.Vector(1.0f, Rand.RandSync.Server); + position += offsetDir * Rand.Range(prefab.RandomOffset.X, prefab.RandomOffset.Y, Rand.RandSync.Server); + } + + 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); + + foreach (LevelObjectPrefab.ChildObject child in prefab.ChildObjects) + { + int childCount = Rand.Range(child.MinCount, child.MaxCount, Rand.RandSync.Server); + for (int j = 0; j < childCount; j++) + { + 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; + + Vector2 childPos = position + edgeDir * Rand.Range(-0.5f, 0.5f, Rand.RandSync.Server) * prefab.MinSurfaceWidth; + + var childObject = new LevelObject(childPrefab, + new Vector3(childPos, Rand.Range(childPrefab.DepthRange.X, childPrefab.DepthRange.Y, Rand.RandSync.Server)), + Rand.Range(childPrefab.MinSize, childPrefab.MaxSize, Rand.RandSync.Server), + rotation + Rand.Range(childPrefab.RandomRotationRad.X, childPrefab.RandomRotationRad.Y, Rand.RandSync.Server)); + + AddObject(childObject, level); + } + } } private void AddObject(LevelObject newObject, Level level) { - foreach (LevelTrigger trigger in newObject.Triggers) - { - trigger.OnTriggered += (levelTrigger, obj) => + if (newObject.Triggers != null) + { + foreach (LevelTrigger trigger in newObject.Triggers) { - OnObjectTriggered(newObject, levelTrigger, obj); - }; + trigger.OnTriggered += (levelTrigger, obj) => + { + OnObjectTriggered(newObject, levelTrigger, obj); + }; + } } var spriteCorners = new List @@ -223,22 +302,24 @@ namespace Barotrauma float minY = spriteCorners.Min(c => c.Y) - newObject.Position.Z - level.BottomPos; float maxY = spriteCorners.Max(c => c.Y) + newObject.Position.Z - level.BottomPos; - foreach (LevelTrigger trigger in newObject.Triggers) + if (newObject.Triggers != null) { - if (trigger.PhysicsBody == null) continue; - for (int i = 0; i < trigger.PhysicsBody.FarseerBody.FixtureList.Count; i++) + foreach (LevelTrigger trigger in newObject.Triggers) { - trigger.PhysicsBody.FarseerBody.GetTransform(out FarseerPhysics.Common.Transform transform); - trigger.PhysicsBody.FarseerBody.FixtureList[i].Shape.ComputeAABB(out FarseerPhysics.Collision.AABB aabb, ref transform, i); + if (trigger.PhysicsBody == null) { continue; } + for (int i = 0; i < trigger.PhysicsBody.FarseerBody.FixtureList.Count; i++) + { + trigger.PhysicsBody.FarseerBody.GetTransform(out FarseerPhysics.Common.Transform transform); + trigger.PhysicsBody.FarseerBody.FixtureList[i].Shape.ComputeAABB(out FarseerPhysics.Collision.AABB aabb, ref transform, i); - minX = Math.Min(minX, ConvertUnits.ToDisplayUnits(aabb.LowerBound.X)); - maxX = Math.Max(maxX, ConvertUnits.ToDisplayUnits(aabb.UpperBound.X)); - minY = Math.Min(minY, ConvertUnits.ToDisplayUnits(aabb.LowerBound.Y) - level.BottomPos); - maxY = Math.Max(maxY, ConvertUnits.ToDisplayUnits(aabb.UpperBound.Y) - level.BottomPos); + minX = Math.Min(minX, ConvertUnits.ToDisplayUnits(aabb.LowerBound.X)); + maxX = Math.Max(maxX, ConvertUnits.ToDisplayUnits(aabb.UpperBound.X)); + minY = Math.Min(minY, ConvertUnits.ToDisplayUnits(aabb.LowerBound.Y) - level.BottomPos); + maxY = Math.Max(maxY, ConvertUnits.ToDisplayUnits(aabb.UpperBound.Y) - level.BottomPos); + } } } - #if CLIENT if (newObject.ParticleEmitters != null) { @@ -253,6 +334,7 @@ namespace Barotrauma } #endif objects.Add(newObject); + if (newObject.NeedsUpdate) { updateableObjects.Add(newObject); } newObject.Position.Z += (minX + minY) % 100.0f * 0.00001f; int xStart = (int)Math.Floor(minX / GridSize); @@ -290,7 +372,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); @@ -309,10 +391,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); } } } @@ -322,12 +404,14 @@ namespace Barotrauma private List GetAvailableSpawnPositions(IEnumerable cells, LevelObjectPrefab.SpawnPosType spawnPosType) { + List spawnPosTypes = new List(4); List availableSpawnPositions = new List(); foreach (var cell in cells) { foreach (var edge in cell.Edges) { - if (!edge.IsSolid || edge.OutsideLevel) continue; + if (!edge.IsSolid || edge.OutsideLevel) { continue; } + if (spawnPosType != LevelObjectPrefab.SpawnPosType.CaveWall && edge.NextToCave) { continue; } Vector2 normal = edge.GetNormal(cell); Alignment edgeAlignment = 0; @@ -340,7 +424,13 @@ namespace Barotrauma else if(normal.X > 0.5f) edgeAlignment |= Alignment.Right; - availableSpawnPositions.Add(new SpawnPosition(edge, normal, spawnPosType, edgeAlignment)); + spawnPosTypes.Clear(); + spawnPosTypes.Add(spawnPosType); + if (spawnPosType.HasFlag(LevelObjectPrefab.SpawnPosType.MainPathWall) && edge.NextToMainPath) { spawnPosTypes.Add(LevelObjectPrefab.SpawnPosType.MainPathWall); } + if (spawnPosType.HasFlag(LevelObjectPrefab.SpawnPosType.SidePathWall) && edge.NextToSidePath) { spawnPosTypes.Add(LevelObjectPrefab.SpawnPosType.SidePathWall); } + if (spawnPosType.HasFlag(LevelObjectPrefab.SpawnPosType.CaveWall) && edge.NextToCave) { spawnPosTypes.Add(LevelObjectPrefab.SpawnPosType.CaveWall); } + + availableSpawnPositions.Add(new SpawnPosition(edge, normal, spawnPosTypes, edgeAlignment)); } } return availableSpawnPositions; @@ -348,7 +438,7 @@ namespace Barotrauma public void Update(float deltaTime) { - foreach (LevelObject obj in objects) + foreach (LevelObject obj in updateableObjects) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { @@ -361,21 +451,24 @@ namespace Barotrauma } } - obj.ActivePrefab = obj.Prefab; - for (int i = 0; i < obj.Triggers.Count; i++) + if (obj.Triggers != null) { - obj.Triggers[i].Update(deltaTime); - if (obj.Triggers[i].IsTriggered && obj.Prefab.OverrideProperties[i] != null) + obj.ActivePrefab = obj.Prefab; + for (int i = 0; i < obj.Triggers.Count; i++) { - obj.ActivePrefab = obj.Prefab.OverrideProperties[i]; + obj.Triggers[i].Update(deltaTime); + if (obj.Triggers[i].IsTriggered && obj.Prefab.OverrideProperties[i] != null) + { + obj.ActivePrefab = obj.Prefab.OverrideProperties[i]; + } } } if (obj.PhysicsBody != null) { - if (obj.Prefab.PhysicsBodyTriggerIndex > -1) obj.PhysicsBody.Enabled = obj.Triggers[obj.Prefab.PhysicsBodyTriggerIndex].IsTriggered; - obj.Position = new Vector3(obj.PhysicsBody.Position, obj.Position.Z); - obj.Rotation = obj.PhysicsBody.Rotation; + if (obj.Prefab.PhysicsBodyTriggerIndex > -1) { obj.PhysicsBody.Enabled = obj.Triggers[obj.Prefab.PhysicsBodyTriggerIndex].IsTriggered; } + /*obj.Position = new Vector3(obj.PhysicsBody.Position, obj.Position.Z); + obj.Rotation = -obj.PhysicsBody.Rotation;*/ } } @@ -386,10 +479,10 @@ namespace Barotrauma private void OnObjectTriggered(LevelObject triggeredObject, LevelTrigger trigger, Entity triggerer) { - if (trigger.TriggerOthersDistance <= 0.0f) return; + if (trigger.TriggerOthersDistance <= 0.0f) { return; } foreach (LevelObject obj in objects) { - if (obj == triggeredObject) continue; + if (obj == triggeredObject || obj.Triggers == null) { continue; } foreach (LevelTrigger otherTrigger in obj.Triggers) { otherTrigger.OtherTriggered(triggeredObject, trigger); @@ -397,16 +490,20 @@ namespace Barotrauma } } - private LevelObjectPrefab GetRandomPrefab(string levelType) - { - return GetRandomPrefab(levelType, LevelObjectPrefab.List); - } - - private LevelObjectPrefab GetRandomPrefab(string levelType, IList availablePrefabs) + private LevelObjectPrefab GetRandomPrefab(LevelGenerationParams generationParams, IList availablePrefabs) { + if (availablePrefabs.Sum(p => p.GetCommonness(generationParams)) <= 0.0f) { return null; } return ToolBox.SelectWeightedRandom( availablePrefabs, - availablePrefabs.Select(p => p.GetCommonness(levelType)).ToList(), Rand.RandSync.Server); + availablePrefabs.Select(p => p.GetCommonness(generationParams)).ToList(), Rand.RandSync.Server); + } + + private LevelObjectPrefab GetRandomPrefab(CaveGenerationParams caveParams, IList availablePrefabs) + { + if (availablePrefabs.Sum(p => p.GetCommonness(caveParams)) <= 0.0f) { return null; } + return ToolBox.SelectWeightedRandom( + availablePrefabs, + availablePrefabs.Select(p => p.GetCommonness(caveParams)).ToList(), Rand.RandSync.Server); } public override void Remove() @@ -418,6 +515,7 @@ namespace Barotrauma obj.Remove(); } objects.Clear(); + updateableObjects.Clear(); } RemoveProjSpecific(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs index dbf0b3af4..552d90a9c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs @@ -8,11 +8,7 @@ namespace Barotrauma { partial class LevelObjectPrefab : ISerializableEntity { - private static List list = new List(); - public static List List - { - get { return list; } - } + public static List List { get; } = new List(); public class ChildObject { @@ -38,12 +34,15 @@ namespace Barotrauma public enum SpawnPosType { None = 0, - Wall = 1, - RuinWall = 2, - SeaFloor = 4, - MainPath = 8, - LevelStart = 16, - LevelEnd = 32, + MainPathWall = 1, + SidePathWall = 2, + CaveWall = 4, + RuinWall = 8, + SeaFloor = 16, + MainPath = 32, + LevelStart = 64, + LevelEnd = 128, + Wall = MainPathWall | SidePathWall | CaveWall, } public List Sprites @@ -52,12 +51,6 @@ namespace Barotrauma private set; } = new List(); - public List SpecularSprites - { - get; - private set; - } = new List(); - public DeformableSprite DeformableSprite { get; @@ -117,7 +110,13 @@ namespace Barotrauma { get; private set; - } + } = -1; + + public Dictionary SpriteSpecificPhysicsBodyElements + { + get; + private set; + } = new Dictionary(); [Serialize(10000, false, description: "Maximum number of this specific object per level."), Editable(MinValueFloat = 0.01f, MaxValueFloat = 10.0f)] @@ -162,6 +161,13 @@ namespace Barotrauma private set; } + [Editable, Serialize("0,0", true, description: "Random offset from the surface the object spawns on.")] + public Vector2 RandomOffset + { + get; + private set; + } + [Editable, Serialize(false, true, description: "Should the object be rotated to align it with the wall surface it spawns on.")] public bool AlignWithSurface { @@ -243,12 +249,18 @@ namespace Barotrauma private set; } - public string Name + public string Identifier { get; set; } + + public string Name + { + get { return Identifier; } + } + public List ChildObjects { get; @@ -272,12 +284,12 @@ namespace Barotrauma public override string ToString() { - return "LevelObjectPrefab (" + Name + ")"; + return "LevelObjectPrefab (" + Identifier + ")"; } public static void LoadAll() { - list.Clear(); + List.Clear(); var files = GameMain.Instance.GetFilesOfType(ContentType.LevelObjectPrefabs); if (files.Count() > 0) { @@ -303,35 +315,62 @@ namespace Barotrauma { mainElement = doc.Root.FirstElement(); DebugConsole.NewMessage($"Overriding all level object prefabs with '{configPath}'", Color.Yellow); - list.Clear(); + List.Clear(); } - else if (list.Any()) + 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 element in mainElement.Elements()) + foreach (XElement subElement in mainElement.Elements()) { - list.Add(new LevelObjectPrefab(element)); + var element = subElement.IsOverride() ? subElement.FirstElement() : subElement; + string identifier = element.GetAttributeString("identifier", ""); + var existingPrefab = List.Find(p => p.Identifier.Equals(identifier, StringComparison.OrdinalIgnoreCase)); + if (existingPrefab != null) + { + if (subElement.IsOverride()) + { + DebugConsole.NewMessage($"Overriding the existing level object prefab '{identifier}' using the file '{configPath}'", Color.Yellow); + List.Remove(existingPrefab); + } + else + { + DebugConsole.ThrowError($"Error in '{configPath}': Duplicate level object prefab '{identifier}' found in '{configPath}'! Each level object prefab must have a unique identifier. " + + "Use tags to override prefabs."); + continue; + } + } + List.Add(new LevelObjectPrefab(element)); } } catch (Exception e) { - DebugConsole.ThrowError(String.Format("Failed to load LevelObject prefabs from {0}", configPath), e); + DebugConsole.ThrowError(string.Format("Failed to load LevelObject prefabs from {0}", configPath), e); } } - public LevelObjectPrefab(XElement element) + public LevelObjectPrefab(XElement element, string identifier = null) { ChildObjects = new List(); LevelTriggerElements = new List(); OverrideProperties = new List(); OverrideCommonness = new Dictionary(); + Identifier = null; SerializableProperties = SerializableProperty.DeserializeProperties(this, element); if (element != null) { Config = element; - Name = element.Name.ToString(); + Identifier = element.GetAttributeString("identifier", null) ?? identifier; + if (string.IsNullOrEmpty(Identifier)) + { +#if DEBUG + DebugConsole.ThrowError($"Level object prefab \"{element.Name}\" has no identifier! Using the name as the identifier instead..."); +#else + DebugConsole.AddWarning($"Level object prefab \"{element.Name}\" has no identifier! Using the name as the identifier instead..."); +#endif + Identifier = element.Name.ToString(); + } LoadElements(element, -1); InitProjSpecific(element); } @@ -346,21 +385,27 @@ namespace Barotrauma private void LoadElements(XElement element, int parentTriggerIndex) { + int propertyOverrideCount = 0; foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "sprite": - Sprites.Add( new Sprite(subElement, lazyLoad: true)); - break; - case "specularsprite": - SpecularSprites.Add(new Sprite(subElement, lazyLoad: true)); + var newSprite = new Sprite(subElement, lazyLoad: true); + Sprites.Add(newSprite); + var spriteSpecificPhysicsBodyElement = + subElement.Element("PhysicsBody") ?? subElement.Element("Body") ?? + subElement.Element("physicsbody") ?? subElement.Element("body"); + if (spriteSpecificPhysicsBodyElement != null) + { + SpriteSpecificPhysicsBodyElements.Add(newSprite, spriteSpecificPhysicsBodyElement); + } break; case "deformablesprite": DeformableSprite = new DeformableSprite(subElement, lazyLoad: true); break; case "overridecommonness": - string levelType = subElement.GetAttributeString("leveltype", ""); + string levelType = subElement.GetAttributeString("leveltype", "").ToLowerInvariant(); if (!OverrideCommonness.ContainsKey(levelType)) { OverrideCommonness.Add(levelType, subElement.GetAttributeFloat("commonness", 1.0f)); @@ -376,13 +421,14 @@ namespace Barotrauma ChildObjects.Add(new ChildObject(subElement)); break; case "overrideproperties": - var propertyOverride = new LevelObjectPrefab(subElement); + var propertyOverride = new LevelObjectPrefab(subElement, identifier: Identifier + "-" + propertyOverrideCount); OverrideProperties[OverrideProperties.Count - 1] = propertyOverride; if (!propertyOverride.Sprites.Any() && propertyOverride.DeformableSprite == null) { propertyOverride.Sprites = Sprites; propertyOverride.DeformableSprite = DeformableSprite; } + propertyOverrideCount++; break; case "body": case "physicsbody": @@ -395,13 +441,26 @@ namespace Barotrauma partial void InitProjSpecific(XElement element); - public float GetCommonness(string levelType) + + public float GetCommonness(CaveGenerationParams generationParams) { - if (!OverrideCommonness.TryGetValue(levelType, out float commonness)) + if (generationParams?.Identifier != null && + OverrideCommonness.TryGetValue(generationParams.Identifier, out float commonness)) { - return Commonness; + return commonness; } - return commonness; + return 0.0f; + } + + public float GetCommonness(LevelGenerationParams generationParams) + { + if (generationParams?.Identifier != null && + (OverrideCommonness.TryGetValue(generationParams.Identifier, out float commonness) || + (generationParams.OldIdentifier != null && OverrideCommonness.TryGetValue(generationParams.OldIdentifier, out commonness)))) + { + return commonness; + } + return Commonness; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs index 951521cbc..171ae1b46 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs @@ -186,6 +186,18 @@ namespace Barotrauma get; set; } + + public string InfectIdentifier + { + get; + set; + } + + public float InfectionChance + { + get; + set; + } public LevelTrigger(XElement element, Vector2 position, float rotation, float scale = 1.0f, string parentDebugName = "") { @@ -211,6 +223,9 @@ namespace Barotrauma } cameraShake = element.GetAttributeFloat("camerashake", 0.0f); + + InfectIdentifier = element.GetAttributeString("infectidentifier", null); + InfectionChance = element.GetAttributeFloat("infectionchance", 0.05f); stayTriggeredDelay = element.GetAttributeFloat("staytriggereddelay", 0.0f); randomTriggerInterval = element.GetAttributeFloat("randomtriggerinterval", 0.0f); @@ -513,9 +528,14 @@ namespace Barotrauma float structureDamage = attack.GetStructureDamage(deltaTime); if (structureDamage > 0.0f) { - Explosion.RangedStructureDamage(worldPosition, attack.DamageRange, structureDamage); + Explosion.RangedStructureDamage(worldPosition, attack.DamageRange, structureDamage, levelWallDamage: 0.0f); } } + + if (!string.IsNullOrWhiteSpace(InfectIdentifier)) + { + submarine.AttemptBallastFloraInfection(InfectIdentifier, deltaTime, InfectionChance); + } } if (Force.LengthSquared() > 0.01f) @@ -598,7 +618,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 5fdb2ebec..f4d08b8a0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelWall.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelWall.cs @@ -4,6 +4,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using Voronoi2; +using System.Linq; #if CLIENT using Microsoft.Xna.Framework.Graphics; #endif @@ -11,18 +12,15 @@ using Microsoft.Xna.Framework.Graphics; namespace Barotrauma { partial class LevelWall : IDisposable - { - private List cells; - public List Cells - { - get { return cells; } - } + { + public List Cells { get; private set; } - private Body body; - public Body Body - { - get { return body; } - } + public Body Body { get; private set; } + + protected readonly Level level; + + private readonly List triangles; + private readonly Color color; private float moveState; private float moveLength; @@ -38,6 +36,17 @@ namespace Barotrauma } } + 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; private Vector2? originalPos; @@ -50,30 +59,32 @@ namespace Barotrauma public LevelWall(List vertices, Color color, Level level, bool giftWrap = false) { - if (giftWrap) + this.level = level; + this.color = color; + List originalVertices = new List(vertices); + if (giftWrap) { vertices = MathUtils.GiftWrap(vertices); } + if (vertices.Count < 3) { - vertices = MathUtils.GiftWrap(vertices); + throw new ArgumentException("Failed to generate a wall (not enough vertices). Original vertices: " + string.Join(", ", originalVertices.Select(v => v.ToString()))); } - VoronoiCell wallCell = new VoronoiCell(vertices.ToArray()); for (int i = 0; i < wallCell.Edges.Count; i++) { wallCell.Edges[i].Cell1 = wallCell; wallCell.Edges[i].IsSolid = true; } - cells = new List() { wallCell }; - - body = CaveGenerator.GeneratePolygons(cells, level, out List triangles); + Cells = new List() { wallCell }; + Body = CaveGenerator.GeneratePolygons(Cells, level, out triangles); #if CLIENT - List bodyVertices = CaveGenerator.GenerateRenderVerticeList(triangles); - SetBodyVertices(bodyVertices.ToArray(), color); - SetWallVertices(CaveGenerator.GenerateWallShapes(cells, level), color); + GenerateVertices(); #endif } public LevelWall(List edgePositions, Vector2 extendAmount, Color color, Level level) { - cells = new List(); + this.level = level; + this.color = color; + Cells = new List(); for (int i = 0; i < edgePositions.Count - 1; i++) { Vector2[] vertices = new Vector2[4]; @@ -84,7 +95,7 @@ namespace Barotrauma VoronoiCell wallCell = new VoronoiCell(vertices) { - CellType = CellType.Edge + CellType = CellType.Solid }; wallCell.Edges[0].Cell1 = wallCell; wallCell.Edges[1].Cell1 = wallCell; @@ -94,31 +105,28 @@ namespace Barotrauma if (i > 1) { - wallCell.Edges[3].Cell2 = cells[i - 1]; - cells[i - 1].Edges[1].Cell2 = wallCell; + wallCell.Edges[3].Cell2 = Cells[i - 1]; + Cells[i - 1].Edges[1].Cell2 = wallCell; } - cells.Add(wallCell); + Cells.Add(wallCell); } - body = CaveGenerator.GeneratePolygons(cells, level, out List triangles); - body.CollisionCategories = Physics.CollisionLevel; - + Body = CaveGenerator.GeneratePolygons(Cells, level, out triangles); + Body.CollisionCategories = Physics.CollisionLevel; #if CLIENT - List bodyVertices = CaveGenerator.GenerateRenderVerticeList(triangles); - SetBodyVertices(bodyVertices.ToArray(), color); - SetWallVertices(CaveGenerator.GenerateWallShapes(cells, level), color); + GenerateVertices(); #endif } - public void Update(float deltaTime) + public virtual void Update(float deltaTime) { - if (body.BodyType == BodyType.Static) return; + if (Body.BodyType == BodyType.Static) { return; } - Vector2 bodyPos = ConvertUnits.ToDisplayUnits(body.Position); + Vector2 bodyPos = ConvertUnits.ToDisplayUnits(Body.Position); Cells.ForEach(c => c.Translation = bodyPos); - if (!originalPos.HasValue) originalPos = bodyPos; + if (!originalPos.HasValue) { originalPos = bodyPos; } if (moveLength > 0.0f && MoveSpeed > 0.0f) { @@ -126,10 +134,15 @@ namespace Barotrauma moveState %= MathHelper.TwoPi; Vector2 targetPos = ConvertUnits.ToSimUnits(originalPos.Value + moveAmount * (float)Math.Sin(moveState)); - body.ApplyForce((targetPos - body.Position).ClampLength(1.0f) * body.Mass); + Body.ApplyForce((targetPos - Body.Position).ClampLength(1.0f) * Body.Mass); } } + public bool IsPointInside(Vector2 point) + { + return Cells.Any(c => c.IsPointInside(point)); + } + public void Dispose() { Dispose(true); @@ -139,16 +152,8 @@ namespace Barotrauma protected virtual void Dispose(bool disposing) { #if CLIENT - if (wallVertices != null) - { - wallVertices.Dispose(); - wallVertices = null; - } - if (bodyVertices != null) - { - BodyVertices.Dispose(); - bodyVertices = null; - } + VertexBuffer?.Dispose(); + VertexBuffer = null; #endif } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs index b97ca1b85..1b15bb666 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs @@ -31,7 +31,7 @@ namespace Barotrauma.RuinGeneration private string filePath; - private List roomTypeList; + private readonly List roomTypeList; public string Name => "RuinGenerationParams"; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs index c25e2a8e7..2e0acdcf5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs @@ -71,8 +71,8 @@ namespace Barotrauma } } - public LinkedSubmarine(Submarine submarine) - : base(null, submarine) + public LinkedSubmarine(Submarine submarine, ushort id = Entity.NullEntityID) + : base(null, submarine, id) { linkedToID = new List(); @@ -102,9 +102,9 @@ namespace Barotrauma return sl; } - public static LinkedSubmarine CreateDummy(Submarine mainSub, XElement element, Vector2 position) + public static LinkedSubmarine CreateDummy(Submarine mainSub, XElement element, Vector2 position, ushort id = Entity.NullEntityID) { - LinkedSubmarine sl = new LinkedSubmarine(mainSub); + LinkedSubmarine sl = new LinkedSubmarine(mainSub, id); sl.GenerateWallVertices(element); if (sl.wallVertices.Any()) { @@ -132,15 +132,7 @@ namespace Barotrauma public override MapEntity Clone() { - - var path = filePath; - if (string.IsNullOrEmpty(path)) - { - var linkedSubmarine = CreateDummy(Submarine, saveElement, Position); - linkedSubmarine.saveElement = saveElement; - return linkedSubmarine; - } - return CreateDummy(Submarine, path, Position); + return CreateDummy(Submarine, filePath, Position); } private void GenerateWallVertices(XElement rootElement) @@ -171,13 +163,13 @@ namespace Barotrauma } // LinkedSubmarine.Load() is called from MapEntity.LoadAll() - public static LinkedSubmarine Load(XElement element, Submarine submarine) + public static LinkedSubmarine Load(XElement element, Submarine submarine, IdRemap idRemap) { Vector2 pos = element.GetAttributeVector2("pos", Vector2.Zero); LinkedSubmarine linkedSub; if (Screen.Selected == GameMain.SubEditorScreen) { - linkedSub = CreateDummy(submarine, element, pos); + linkedSub = CreateDummy(submarine, element, pos, idRemap.AssignMaxId()); linkedSub.saveElement = element; linkedSub.purchasedLostShuttles = false; } @@ -185,7 +177,7 @@ namespace Barotrauma { string levelSeed = element.GetAttributeString("location", ""); LevelData levelData = GameMain.GameSession.Campaign?.NextLevel ?? GameMain.GameSession.LevelData; - linkedSub = new LinkedSubmarine(submarine) + linkedSub = new LinkedSubmarine(submarine, idRemap.AssignMaxId()) { purchasedLostShuttles = GameMain.GameSession.GameMode is CampaignMode campaign && campaign.PurchasedLostShuttles, saveElement = element @@ -207,9 +199,9 @@ namespace Barotrauma int[] linkedToIds = element.GetAttributeIntArray("linkedto", new int[0]); for (int i = 0; i < linkedToIds.Length; i++) { - linkedSub.linkedToID.Add((ushort)linkedToIds[i]); + linkedSub.linkedToID.Add(idRemap.GetOffsetId(linkedToIds[i])); } - linkedSub.originalLinkedToID = (ushort)element.GetAttributeInt("originallinkedto", 0); + linkedSub.originalLinkedToID = idRemap.GetOffsetId(element.GetAttributeInt("originallinkedto", 0)); linkedSub.originalMyPortID = (ushort)element.GetAttributeInt("originalmyport", 0); return linkedSub.loadSub ? linkedSub : null; @@ -238,8 +230,11 @@ namespace Barotrauma return; } - sub = Submarine.Load(info, false); - + IdRemap parentRemap = new IdRemap(Submarine.Info.SubmarineElement, Submarine.IdOffset); + sub = Submarine.Load(info, false, parentRemap); + + IdRemap childRemap = new IdRemap(saveElement, sub.IdOffset); + Vector2 worldPos = saveElement.GetAttributeVector2("worldpos", Vector2.Zero); if (worldPos != Vector2.Zero) { @@ -273,7 +268,8 @@ namespace Barotrauma } originalLinkedPort = linkedPort; - myPort = (FindEntityByID(originalMyPortID) as Item)?.GetComponent(); + ushort originalMyId = childRemap.GetOffsetId(originalMyPortID); + myPort = (FindEntityByID(originalMyId) as Item)?.GetComponent(); if (myPort == null) { float closestDistance = 0.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index 6541c07df..a3c2000c4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -33,7 +33,7 @@ namespace Barotrauma { OriginalContainerID = item.OriginalContainerID; } - OriginalID = item.OriginalID; + OriginalID = item.ID; ModuleIndex = (ushort)item.OriginalModuleIndex; Identifier = item.prefab.Identifier; } @@ -50,7 +50,7 @@ namespace Barotrauma } else { - return item.OriginalID == OriginalID && item.OriginalModuleIndex == ModuleIndex && item.prefab.Identifier == Identifier; + return item.ID == OriginalID && item.OriginalModuleIndex == ModuleIndex && item.prefab.Identifier == Identifier; } } } @@ -76,17 +76,6 @@ namespace Barotrauma public LevelData LevelData { get; set; } - private float normalizedDepth; - public float NormalizedDepth - { - get { return normalizedDepth; } - set - { - if (!MathUtils.IsValid(value)) { return; } - normalizedDepth = MathHelper.Clamp(value, 0.0f, 1.0f); - } - } - public int PortraitId { get; private set; } public Reputation Reputation { get; set; } @@ -200,7 +189,6 @@ namespace Barotrauma baseName = element.GetAttributeString("basename", ""); Name = element.GetAttributeString("name", ""); MapPosition = element.GetAttributeVector2("position", Vector2.Zero); - NormalizedDepth = element.GetAttributeFloat("normalizeddepth", 0.0f); TypeChangeTimer = element.GetAttributeInt("changetimer", 0); Discovered = element.GetAttributeBool("discovered", false); PriceMultiplier = element.GetAttributeFloat("pricemultiplier", 1.0f); @@ -285,14 +273,15 @@ namespace Barotrauma DebugConsole.Log("Location " + baseName + " changed it's type from " + Type + " to " + newType); Type = newType; - Name = Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", baseName); + Name = Type.NameFormats == null ? baseName : Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", baseName); CreateStore(force: true); } public void UnlockMission(MissionPrefab missionPrefab, LocationConnection connection) { if (AvailableMissions.Any(m => m.Prefab == missionPrefab)) { return; } - availableMissions.Add(InstantiateMission(missionPrefab, connection)); + var mission = InstantiateMission(missionPrefab, connection); + availableMissions.Add(mission); #if CLIENT GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo(); #endif @@ -309,7 +298,7 @@ namespace Barotrauma } else { - var mission = InstantiateMission(missionPrefab); + 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]))) { @@ -342,7 +331,7 @@ namespace Barotrauma suitableMissions = unusedMissions; } MissionPrefab missionPrefab = suitableMissions.GetRandom(); - var mission = InstantiateMission(missionPrefab); + 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]))) { @@ -363,24 +352,31 @@ namespace Barotrauma return null; } - private Mission InstantiateMission(MissionPrefab prefab, LocationConnection connection = null) + private Mission InstantiateMission(MissionPrefab prefab, out LocationConnection connection) { - if (connection == null) + var suitableConnections = Connections.Where(c => prefab.IsAllowed(this, c.OtherLocation(this))); + if (!suitableConnections.Any()) { - var suitableConnections = Connections.Where(c => prefab.IsAllowed(this, c.OtherLocation(this))); - if (!suitableConnections.Any()) - { - suitableConnections = Connections; - } - //prefer connections that haven't been passed through, and connections with fewer available missions - 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); + suitableConnections = Connections.ToList(); } + //prefer connections that haven't been passed through, and connections with fewer available missions + 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); Location destination = connection.OtherLocation(this); - return prefab.Instantiate(new Location[] { this, destination }); + var mission = prefab.Instantiate(new Location[] { this, destination }); + mission.AdjustLevelData(connection.LevelData); + return mission; + } + + 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; } public void InstantiateLoadedMissions(Map map) @@ -499,7 +495,7 @@ namespace Barotrauma { foreach (Item item in items) { - if (takenItems.Any(it => it.Matches(item) && it.OriginalID == item.OriginalID)) { continue; } + if (takenItems.Any(it => it.Matches(item) && it.OriginalID == item.ID)) { continue; } if (item.OriginalModuleIndex < 0) { DebugConsole.ThrowError("Tried to register a non-outpost item as being taken from the outpost."); @@ -698,7 +694,6 @@ namespace Barotrauma new XAttribute("name", Name), new XAttribute("discovered", Discovered), new XAttribute("position", XMLExtensions.Vector2ToString(MapPosition)), - new XAttribute("normalizeddepth", NormalizedDepth.ToString("G", CultureInfo.InvariantCulture)), new XAttribute("pricemultiplier", PriceMultiplier), new XAttribute("mechanicalpricemultipler", MechanicalPriceMultiplier)); LevelData.Save(locationElement); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs index b5eaa3441..cb91b64a3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs @@ -30,6 +30,8 @@ namespace Barotrauma public readonly string Identifier; public readonly string Name; + public readonly float BeaconStationChance; + public readonly List CanChangeTo = new List(); public bool UseInMainMenu @@ -74,6 +76,9 @@ namespace Barotrauma { Identifier = element.GetAttributeString("identifier", element.Name.ToString()); Name = TextManager.Get("LocationName." + Identifier, fallBackTag: "unknown"); + + BeaconStationChance = element.GetAttributeFloat("beaconstationchance", 0.0f); + nameFormats = TextManager.GetAll("LocationNameFormat." + Identifier); UseInMainMenu = element.GetAttributeBool("useinmainmenu", false); HasOutpost = element.GetAttributeBool("hasoutpost", true); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs index 85f101d09..fc569425f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Xml.Linq; @@ -7,9 +8,15 @@ namespace Barotrauma class LocationTypeChange { public readonly string ChangeToType; + public readonly float Probability; public readonly int RequiredDuration; + public readonly float ProximityProbabilityIncrease; + public readonly int RequiredProximityForProbabilityIncrease; + + public readonly bool RequireDiscovered; + public List Messages = new List(); //the change can't happen if there's a location of the given type next to this one @@ -24,6 +31,11 @@ namespace Barotrauma Probability = element.GetAttributeFloat("probability", 1.0f); RequiredDuration = element.GetAttributeInt("requiredduration", 0); + RequireDiscovered = element.GetAttributeBool("requirediscovered", false); + + ProximityProbabilityIncrease = element.GetAttributeFloat("proximityprobabilityincrease", 0.0f); + RequiredProximityForProbabilityIncrease = element.GetAttributeInt("requiredproximityforprobabilityincrease", -1); + DisallowedAdjacentLocations = element.GetAttributeStringArray("disallowedadjacentlocations", new string[0]).ToList(); RequiredAdjacentLocations = element.GetAttributeStringArray("requiredadjacentlocations", new string[0]).ToList(); @@ -35,5 +47,49 @@ namespace Barotrauma DebugConsole.ThrowError("No messages defined for the location type change " + currentType + " -> " + ChangeToType); } } + + public float DetermineProbability(Location location) + { + float totalProbability = Probability; + if (AnyWithinRequiredProximity(location)) { totalProbability += ProximityProbabilityIncrease; } + return totalProbability; + } + + private bool AnyWithinRequiredProximity(Location location, int currentDistance = 0, HashSet checkedLocations = null) + { + if (currentDistance > RequiredProximityForProbabilityIncrease) { return false; } + if (currentDistance > 0 && RequiredAdjacentLocations.Contains(location.Type.Identifier)) { return true; } + + checkedLocations ??= new HashSet(); + checkedLocations.Add(location); + + foreach (var connection in location.Connections) + { + var otherLocation = connection.OtherLocation(location); + if (!checkedLocations.Contains(otherLocation)) + { + if (AnyWithinRequiredProximity(otherLocation, currentDistance + 1, checkedLocations)) { return true; } + } + } + + return false; + } + + private int CountWithinRequiredProximity(Location location, int currentDistance = 0, HashSet checkedLocations = null) + { + if (currentDistance > RequiredProximityForProbabilityIncrease) { return 0; } + int count = currentDistance > 0 && RequiredAdjacentLocations.Contains(location.Type.Identifier) ? 1 : 0; + + checkedLocations ??= new HashSet(); + checkedLocations.Add(location); + + foreach (var connection in location.Connections) + { + var otherLocation = connection.OtherLocation(location); + if (!checkedLocations.Contains(otherLocation)) { count += CountWithinRequiredProximity(otherLocation, currentDistance+1, checkedLocations); } + } + + return count; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index f5c8ad934..aff3e4fc1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -69,6 +69,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 +81,14 @@ 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; } } 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()) { @@ -101,7 +105,10 @@ namespace Barotrauma Locations[locationIndices.Y].Connections.Add(connection); connection.LevelData = new LevelData(subElement.Element("Level")); string biomeId = subElement.GetAttributeString("biome", ""); - connection.Biome = LevelGenerationParams.GetBiomes().FirstOrDefault(b => b.Identifier == biomeId) ?? LevelGenerationParams.GetBiomes().First(); + connection.Biome = + LevelGenerationParams.GetBiomes().FirstOrDefault(b => b.Identifier == biomeId) ?? + LevelGenerationParams.GetBiomes().FirstOrDefault(b => b.OldIdentifier == biomeId) ?? + LevelGenerationParams.GetBiomes().First(); Connections.Add(connection); break; } @@ -354,9 +361,8 @@ namespace Barotrauma CreateEndLocation(); foreach (Location location in Locations) - { + { location.LevelData = new LevelData(location); - location.NormalizedDepth = location.MapPosition.X / Width; } foreach (LocationConnection connection in Connections) { @@ -672,9 +678,16 @@ namespace Barotrauma { foreach (Location location in Locations) { - if (!location.Discovered) { continue; } + if (furthestDiscoveredLocation == null || + location.MapPosition.X > furthestDiscoveredLocation.MapPosition.X) + { + furthestDiscoveredLocation = location; + } + } - if (furthestDiscoveredLocation == null || location.MapPosition.X > furthestDiscoveredLocation.MapPosition.X) + foreach (Location location in Locations) + { + if (location.MapPosition.X < furthestDiscoveredLocation.MapPosition.X) { furthestDiscoveredLocation = location; } @@ -682,10 +695,13 @@ namespace Barotrauma 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(); - foreach (LocationTypeChange typeChange in location.Type.CanChangeTo) + List readyTypeChanges = new List(); + for (int i = 0; i < cct.Count; i++) { + LocationTypeChange typeChange = cct[i]; + if (typeChange.RequireDiscovered && !location.Discovered) { continue; } //check if there are any adjacent locations that would prevent the change bool disallowedFound = false; foreach (string disallowedLocationName in typeChange.DisallowedAdjacentLocations) @@ -714,22 +730,26 @@ namespace Barotrauma if (location.TypeChangeTimer >= typeChange.RequiredDuration) { - readyTypeChanges.Add(typeChange); + readyTypeChanges.Add(i); } } + List readyTypeProbabilities = readyTypeChanges.Select(i => cct[i].DetermineProbability(location)).ToList(); //select a random type change - if (Rand.Range(0.0f, 1.0f) < readyTypeChanges.Sum(t => t.Probability)) + if (Rand.Range(0.0f, 1.0f) < readyTypeChanges.Sum(i => readyTypeProbabilities[i])) { - var selectedTypeChange = - ToolBox.SelectWeightedRandom(readyTypeChanges, readyTypeChanges.Select(t => t.Probability).ToList(), Rand.RandSync.Unsynced); + var selectedTypeChangeIndex = + ToolBox.SelectWeightedRandom( + readyTypeChanges, + readyTypeChanges.Select(i => readyTypeProbabilities[i]).ToList(), + Rand.RandSync.Unsynced); + var selectedTypeChange = cct[selectedTypeChangeIndex]; if (selectedTypeChange != null) { string prevName = location.Name; location.ChangeType(LocationType.List.Find(lt => lt.Identifier.Equals(selectedTypeChange.ChangeToType, StringComparison.OrdinalIgnoreCase))); ChangeLocationType(location, prevName, selectedTypeChange); location.TypeChangeTimer = -1; - break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs index 43ef0f908..a70672a87 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -12,13 +12,14 @@ using Barotrauma.Networking; namespace Barotrauma { - abstract partial class MapEntity : Entity + abstract partial class MapEntity : Entity, ISpatialEntity { public static List mapEntityList = new List(); public readonly MapEntityPrefab prefab; protected List linkedToID; + public List unresolvedLinkedToID; /// /// List of upgrades this item has @@ -249,12 +250,55 @@ namespace Barotrauma get { return ""; } } - public MapEntity(MapEntityPrefab prefab, Submarine submarine) : base(submarine) + 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) { this.prefab = prefab; Scale = prefab != null ? prefab.Scale : 1; } + protected void ParseLinks(XElement element, IdRemap idRemap) + { + string linkedToString = element.GetAttributeString("linked", ""); + if (!string.IsNullOrEmpty(linkedToString)) + { + string[] linkedToIds = linkedToString.Split(','); + for (int i = 0; i < linkedToIds.Length; i++) + { + int srcId = int.Parse(linkedToIds[i]); + int targetId = idRemap.GetOffsetId(srcId); + if (targetId <= 0) + { + unresolvedLinkedToID ??= new List(); + unresolvedLinkedToID.Add((ushort)srcId); + continue; + } + linkedToID.Add((ushort)targetId); + } + } + } + + public void ResolveLinks(IdRemap childRemap) + { + if (unresolvedLinkedToID == null) { return; } + for (int i=0;i 0) + { + var otherEntity = FindEntityByID((ushort)targetId) as MapEntity; + linkedTo.Add(otherEntity); + if (otherEntity.Linkable && otherEntity.linkedTo != null) otherEntity.linkedTo.Add(this); + unresolvedLinkedToID.RemoveAt(i); + i--; + } + } + } + public virtual void Move(Vector2 amount) { rect.X += (int)amount.X; @@ -555,8 +599,10 @@ namespace Barotrauma Move(-relative * 2.0f); } - public static List LoadAll(Submarine submarine, XElement parentElement, string filePath) + public static List LoadAll(Submarine submarine, XElement parentElement, string filePath, int idOffset) { + IdRemap idRemap = new IdRemap(parentElement, idOffset); + List entities = new List(); foreach (XElement element in parentElement.Elements()) { @@ -580,7 +626,7 @@ namespace Barotrauma try { - MethodInfo loadMethod = t.GetMethod("Load", new[] { typeof(XElement), typeof(Submarine) }); + MethodInfo loadMethod = t.GetMethod("Load", new[] { typeof(XElement), typeof(Submarine), typeof(IdRemap) }); if (loadMethod == null) { DebugConsole.ThrowError("Could not find the method \"Load\" in " + t + "."); @@ -591,7 +637,7 @@ namespace Barotrauma } else { - object newEntity = loadMethod.Invoke(t, new object[] { element, submarine }); + object newEntity = loadMethod.Invoke(t, new object[] { element, submarine, idRemap }); if (newEntity != null) entities.Add((MapEntity)newEntity); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs index 0c60d7b65..a3c0bc602 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs @@ -254,6 +254,16 @@ namespace Barotrauma { name = name.ToLowerInvariant(); } + + //try to search based on identifier first + if (string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(identifier)) + { + foreach (MapEntityPrefab prefab in List) + { + if (prefab.identifier == identifier) { return prefab; } + } + } + foreach (MapEntityPrefab prefab in List) { if (identifier != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index 8d78b85f2..998ad3cd3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs @@ -216,11 +216,13 @@ namespace Barotrauma List loadEntities(Submarine sub) { Dictionary> entities = new Dictionary>(); + int idOffset = sub.IdOffset; for (int i = 0; i < selectedModules.Count; i++) { var selectedModule = selectedModules[i]; sub.Info.GameVersion = selectedModule.Info.GameVersion; - var moduleEntities = MapEntity.LoadAll(sub, selectedModule.Info.SubmarineElement, selectedModule.Info.FilePath); + var moduleEntities = MapEntity.LoadAll(sub, selectedModule.Info.SubmarineElement, selectedModule.Info.FilePath, idOffset); + idOffset = moduleEntities.Max(e => e.ID); MapEntity.InitializeLoadedLinks(moduleEntities); foreach (MapEntity entity in moduleEntities) @@ -958,7 +960,7 @@ namespace Barotrauma return placedEntities; } - var moduleEntities = MapEntity.LoadAll(sub, hallwayInfo.SubmarineElement, hallwayInfo.FilePath); + var moduleEntities = MapEntity.LoadAll(sub, hallwayInfo.SubmarineElement, hallwayInfo.FilePath, -1); //remove items that don't fit in the hallway moduleEntities.Where(e => e is Item item && item.GetComponent() == null && e.Rect.Width > hallwayLength).ForEach(e => e.Remove()); @@ -1418,7 +1420,7 @@ namespace Barotrauma } else { - idleObjective.Behavior = humanPrefab.BehaviorType; + idleObjective.Behavior = humanPrefab.Behavior; foreach (string moduleType in humanPrefab.PreferredOutpostModuleTypes) { idleObjective.PreferredOutpostModuleTypes.Add(moduleType); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index 2dcad4753..426a8ad04 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -14,27 +14,31 @@ using Microsoft.Xna.Framework.Graphics; namespace Barotrauma { - partial class WallSection + partial class WallSection : ISpatialEntity { public Rectangle rect; public float damage; public Gap gap; + private bool ignoreByAI; - public WallSection(Rectangle rect) + public Structure Wall { get; } + public Vector2 Position => Wall.SectionPosition(Wall.Sections.IndexOf(this)); + public Vector2 WorldPosition => Wall.SectionPosition(Wall.Sections.IndexOf(this), world: true); + public Vector2 SimPosition => ConvertUnits.ToSimUnits(Position); + 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 WallSection(Rectangle rect, Structure wall, float damage = 0.0f) { System.Diagnostics.Debug.Assert(rect.Width > 0 && rect.Height > 0); - this.rect = rect; - damage = 0.0f; + this.damage = damage; + Wall = wall; } - public WallSection(Rectangle rect, float damage) - { - System.Diagnostics.Debug.Assert(rect.Width > 0 && rect.Height > 0); - - this.rect = rect; - this.damage = 0.0f; - } + public void SetIgnoreByAI(bool ignore) => ignoreByAI = ignore; } partial class Structure : MapEntity, IDamageable, IServerSerializable, ISerializableEntity @@ -108,7 +112,16 @@ namespace Barotrauma get => maxHealth ?? Prefab.Health; set => maxHealth = value; } - + + private float crushDepth; + + [Serialize(Level.DefaultRealWorldCrushDepth, true)] + public float CrushDepth + { + get => crushDepth; + set => crushDepth = Math.Max(value, Level.DefaultRealWorldCrushDepth); + } + public float Health => MaxHealth; public override bool DrawBelowWater @@ -131,10 +144,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 @@ -343,8 +362,8 @@ namespace Barotrauma #endif } - public Structure(Rectangle rectangle, StructurePrefab sp, Submarine submarine) - : base(sp, submarine) + 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); if (rectangle.Width == 0 || rectangle.Height == 0) { return; } @@ -382,7 +401,6 @@ namespace Barotrauma StairDirection = Prefab.StairDirection; NoAITarget = Prefab.NoAITarget; - SerializableProperties = SerializableProperty.GetProperties(this); InitProjSpecific(); @@ -399,7 +417,7 @@ namespace Barotrauma else { Sections = new WallSection[1]; - Sections[0] = new WallSection(rect); + Sections[0] = new WallSection(rect, this); if (StairDirection != Direction.None) { @@ -408,6 +426,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) { @@ -552,7 +572,7 @@ namespace Barotrauma //sectionRect.Height -= (int)Math.Max((rect.Y - rect.Height) - (sectionRect.Y - sectionRect.Height), 0.0f); int xIndex = FlippedX && IsHorizontal ? (xsections - 1 - x) : x; int yIndex = FlippedY && !IsHorizontal ? (ysections - 1 - y) : y; - Sections[xIndex + yIndex] = new WallSection(sectionRect); + Sections[xIndex + yIndex] = new WallSection(sectionRect, this); } else { @@ -560,7 +580,7 @@ namespace Barotrauma sectionRect.Width -= (int)Math.Max(sectionRect.Right - rect.Right, 0.0f); sectionRect.Height -= (int)Math.Max((rect.Y - rect.Height) - (sectionRect.Y - sectionRect.Height), 0.0f); - Sections[x + y] = new WallSection(sectionRect); + Sections[x + y] = new WallSection(sectionRect, this); } } } @@ -783,33 +803,36 @@ namespace Barotrauma public void AddDamage(int sectionIndex, float damage, Character attacker = null) { - if (!Prefab.Body || Prefab.Platform || Indestructible) return; + if (!Prefab.Body || Prefab.Platform || Indestructible ) { return; } - if (sectionIndex < 0 || sectionIndex > Sections.Length - 1) return; + if (sectionIndex < 0 || sectionIndex > Sections.Length - 1) { return; } var section = Sections[sectionIndex]; #if CLIENT - float dmg = Math.Min(MaxHealth - section.damage, damage); - float particleAmount = MathHelper.Lerp(0, 25, MathUtils.InverseLerp(0, 100, dmg * Rand.Range(0.75f, 1.25f))); - // Special case for very low but frequent dmg like plasma cutter: 10% chance for emitting a particle - if (particleAmount < 1 && Rand.Value() < 0.10f) + if (damage > 0) { - particleAmount = 1; - } - for (int i = 1; i <= particleAmount; i++) - { - Vector2 particlePos = new Vector2( - Rand.Range(section.rect.X, section.rect.Right), - Rand.Range(section.rect.Y - section.rect.Height, section.rect.Y)); - - if (Submarine != null) + float dmg = Math.Min(MaxHealth - section.damage, damage); + float particleAmount = MathHelper.Lerp(0, 25, MathUtils.InverseLerp(0, 100, dmg * Rand.Range(0.75f, 1.25f))); + // Special case for very low but frequent dmg like plasma cutter: 10% chance for emitting a particle + if (particleAmount < 1 && Rand.Value() < 0.10f) { - particlePos += Submarine.DrawPosition; + particleAmount = 1; + } + for (int i = 1; i <= particleAmount; i++) + { + Vector2 particlePos = new Vector2( + Rand.Range(section.rect.X, section.rect.Right), + Rand.Range(section.rect.Y - section.rect.Height, section.rect.Y)); + + if (Submarine != null) + { + particlePos += Submarine.DrawPosition; + } + + var particle = GameMain.ParticleManager.CreateParticle("shrapnel", particlePos, Rand.Vector(Rand.Range(1.0f, 50.0f))); + if (particle == null) break; } - - var particle = GameMain.ParticleManager.CreateParticle("shrapnel", particlePos, Rand.Vector(Rand.Range(1.0f, 50.0f))); - if (particle == null) break; } #endif if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) @@ -895,15 +918,12 @@ namespace Barotrauma } return sectionPos; } - - - } - + } public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = false) { - if (Submarine != null && Submarine.GodMode) return new AttackResult(0.0f, null); - if (!Prefab.Body || Prefab.Platform || Indestructible) return new AttackResult(0.0f, null); + if (Submarine != null && Submarine.GodMode) { return new AttackResult(0.0f, null); } + if (!Prefab.Body || Prefab.Platform || Indestructible) { return new AttackResult(0.0f, null); } Vector2 transformedPos = worldPosition; if (Submarine != null) transformedPos -= Submarine.Position; @@ -921,26 +941,24 @@ namespace Barotrauma GameMain.ParticleManager.CreateParticle("dustcloud", SectionPosition(i), 0.0f, 0.0f); #endif } - } - + } #if CLIENT - if (playSound) + if (playSound && damageAmount > 0) { SoundPlayer.PlayDamageSound(attack.StructureSoundType, damageAmount, worldPosition, tags: Tags); } #endif - 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) { @@ -1054,13 +1072,14 @@ 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) { OnHealthChangedProjSpecific(attacker, damageDiff); @@ -1077,7 +1096,7 @@ namespace Barotrauma bool hasHole = SectionBodyDisabled(sectionIndex); - if (hadHole == hasHole) return; + if (hadHole == hasHole) { return; } UpdateSections(); } @@ -1259,7 +1278,7 @@ namespace Barotrauma } } - public static Structure Load(XElement element, Submarine submarine) + public static Structure Load(XElement element, Submarine submarine, IdRemap idRemap) { string name = element.Attribute("name").Value; string identifier = element.GetAttributeString("identifier", ""); @@ -1272,14 +1291,10 @@ namespace Barotrauma } Rectangle rect = element.GetAttributeRect("rect", Rectangle.Empty); - Structure s = new Structure(rect, prefab, submarine) + Structure s = new Structure(rect, prefab, submarine, idRemap.GetOffsetId(element), element) { Submarine = submarine, - ID = (ushort)int.Parse(element.Attribute("ID").Value) }; - s.OriginalID = s.ID; - - SerializableProperty.DeserializeProperties(s, element); if (submarine?.Info.GameVersion != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index 471148e4a..9f4601332 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -152,6 +152,47 @@ namespace Barotrauma } } + private float? realWorldCrushDepth; + public float RealWorldCrushDepth + { + get + { + if (!realWorldCrushDepth.HasValue) + { + realWorldCrushDepth = float.PositiveInfinity; + foreach (Structure structure in Structure.WallList) + { + if (structure.Submarine != this || !structure.HasBody || structure.Indestructible) { continue; } + realWorldCrushDepth = Math.Min(structure.CrushDepth, realWorldCrushDepth.Value); + } + if (Info.SubmarineClass == SubmarineClass.DeepDiver) + { + realWorldCrushDepth *= 1.2f; + } + } + return realWorldCrushDepth.Value; + } + } + + /// + /// How deep down the sub is from the surface of Europa in meters (affected by level type, does not correspond to "actual" coordinate systems) + /// + public float RealWorldDepth + { + get + { + if (Level.Loaded?.GenerationParams == null) + { + return -WorldPosition.Y * Physics.DisplayToRealWorldRatio; + } + else if (GameMain.GameSession?.Campaign == null) + { + return (-(WorldPosition.Y - Level.Loaded.GenerationParams.Height) + 80000.0f) * Physics.DisplayToRealWorldRatio; + } + return Level.Loaded.GetRealWorldDepth(WorldPosition.Y); + } + } + public bool AtEndPosition { get @@ -210,7 +251,11 @@ namespace Barotrauma public bool AtDamageDepth { - get { return subBody != null && subBody.AtDamageDepth; } + get + { + if (Level.Loaded == null || subBody == null) { return false; } + return RealWorldDepth > Level.Loaded.RealWorldCrushDepth; + } } public override string ToString() @@ -236,6 +281,44 @@ namespace Barotrauma return Math.Max(minPrice, (int)price); } + private float ballastFloraTimer; + public void AttemptBallastFloraInfection(string identifier, float deltaTime, float probability) + { + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } + + if (ballastFloraTimer < 1f) + { + ballastFloraTimer += deltaTime; + return; + } + + ballastFloraTimer = 0; + if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) >= probability) { return; } + + List pumps = new List(); + List allItems = GetItems(true); + + bool anyHasTag = allItems.Any(i => i.HasTag("ballast")); + + foreach (Item item in allItems) + { + if ((!anyHasTag || item.HasTag("ballast")) && item.GetComponent() is { } pump) + { + if (pump.Infected) { continue; } + pumps.Add(pump); + } + } + + if (!pumps.Any()) { return; } + + Pump randomPump = pumps.GetRandom(Rand.RandSync.Unsynced); + randomPump.Infected = true; + randomPump.InfectIdentifier = identifier; +#if SERVER + randomPump.Item.CreateServerEvent(randomPump); +#endif + } + public void MakeWreck() { Info.Type = SubmarineType.Wreck; @@ -732,7 +815,7 @@ namespace Barotrauma /// check visibility between two points (in sim units) /// /// a physics body that was between the points (or null) - public static Body CheckVisibility(Vector2 rayStart, Vector2 rayEnd, bool ignoreLevel = false, bool ignoreSubs = false, bool ignoreSensors = true, bool ignoreDisabledWalls = true) + public static Body CheckVisibility(Vector2 rayStart, Vector2 rayEnd, bool ignoreLevel = false, bool ignoreSubs = false, bool ignoreSensors = true, bool ignoreDisabledWalls = true, bool ignoreBranches = true) { Body closestBody = null; float closestFraction = 1.0f; @@ -753,6 +836,7 @@ namespace Barotrauma && !fixture.CollisionCategories.HasFlag(Physics.CollisionWall) && !fixture.CollisionCategories.HasFlag(Physics.CollisionRepair)) { return -1; } if (ignoreSubs && fixture.Body.UserData is Submarine) { return -1; } + if (ignoreBranches && fixture.Body.UserData is VineTile) { return -1; } if (fixture.Body.UserData as string == "ruinroom") { return -1; } if (fixture.Body.UserData is Structure structure) { @@ -1171,10 +1255,12 @@ namespace Barotrauma return new Rectangle((int)bounds.X, (int)bounds.Y, (int)(bounds.Z - bounds.X), (int)(bounds.Y - bounds.W)); } - public Submarine(SubmarineInfo info, bool showWarningMessages = true, Func> loadEntities = null) : base(null) + public Submarine(SubmarineInfo info, bool showWarningMessages = true, Func> loadEntities = null, IdRemap linkedRemap = null) : base(null, Entity.NullEntityID) { Loading = true; + loaded.Add(this); + Info = new SubmarineInfo(info); ConnectedDockingPorts = new Dictionary(); @@ -1202,7 +1288,7 @@ namespace Barotrauma { if (Info.SubmarineElement != null) { - newEntities = MapEntity.LoadAll(this, Info.SubmarineElement, Info.FilePath); + newEntities = MapEntity.LoadAll(this, Info.SubmarineElement, Info.FilePath, IdOffset); } } else @@ -1211,6 +1297,15 @@ namespace Barotrauma newEntities.ForEach(me => me.Submarine = this); } + if (newEntities != null) + { + foreach (var e in newEntities) + { + if (linkedRemap != null) { e.ResolveLinks(linkedRemap); } + e.unresolvedLinkedToID = null; + } + } + Vector2 center = Vector2.Zero; var matchingHulls = Hull.hullList.FindAll(h => h.Submarine == this); @@ -1279,8 +1374,6 @@ namespace Barotrauma } } - loaded.Add(this); - if (entityGrid != null) { Hull.EntityGrids.Remove(entityGrid); @@ -1290,7 +1383,7 @@ namespace Barotrauma for (int i = 0; i < MapEntity.mapEntityList.Count; i++) { - if (MapEntity.mapEntityList[i].Submarine != this) continue; + if (MapEntity.mapEntityList[i].Submarine != this) { continue; } MapEntity.mapEntityList[i].Move(HiddenSubPosition); } @@ -1313,6 +1406,11 @@ namespace Barotrauma } } + if (GameMain.GameSession?.Campaign?.UpgradeManager != null) + { + GameMain.GameSession.Campaign.UpgradeManager.OnUpgradesChanged += ResetCrushDepth; + } + #if CLIENT GameMain.LightManager.OnMapLoaded(); #endif @@ -1333,19 +1431,27 @@ namespace Barotrauma if (lightComponent != null) lightComponent.LightColor = new Color(lightComponent.LightColor, lightComponent.LightColor.A / 255.0f * 0.5f); } } - - ID = (ushort)(ushort.MaxValue - 1 - Submarine.loaded.IndexOf(this)); } - public static Submarine Load(SubmarineInfo info, bool unloadPrevious) + protected override ushort DetermineID(ushort id, Submarine submarine) + { + return (ushort)(ReservedIDStart - Submarine.loaded.Count); + } + + public static Submarine Load(SubmarineInfo info, bool unloadPrevious, IdRemap linkedRemap = null) { if (unloadPrevious) { Unload(); } - Submarine sub = new Submarine(info, false); + Submarine sub = new Submarine(info, false, linkedRemap: linkedRemap); return sub; } + private void ResetCrushDepth() + { + realWorldCrushDepth = null; + } + public static void RepositionEntities(Vector2 moveAmount, IEnumerable entities) { if (moveAmount.LengthSquared() < 0.00001f) { return; } @@ -1444,7 +1550,7 @@ namespace Barotrauma visibleEntities = null; - if (GameMain.GameScreen.Cam != null) GameMain.GameScreen.Cam.TargetPos = Vector2.Zero; + if (GameMain.GameScreen.Cam != null) { GameMain.GameScreen.Cam.TargetPos = Vector2.Zero; } RemoveAll(); @@ -1482,6 +1588,11 @@ namespace Barotrauma subBody?.Remove(); subBody = null; + if (GameMain.GameSession?.Campaign?.UpgradeManager != null) + { + GameMain.GameSession.Campaign.UpgradeManager.OnUpgradesChanged -= ResetCrushDepth; + } + if (entityGrid != null) { Hull.EntityGrids.Remove(entityGrid); @@ -1516,7 +1627,7 @@ namespace Barotrauma } } - private HashSet obstructedNodes = new HashSet(); + private readonly Dictionary> obstructedNodes = new Dictionary>(); /// /// Permanently disables obstructed waypoints obstructed by the level. @@ -1553,7 +1664,7 @@ namespace Barotrauma { if (otherSub == null) { return; } if (otherSub == this) { return; } - // Check collisions to other subs. Currently only walls are taken into account. + // Check collisions to other subs. foreach (var node in OutdoorNodes) { if (node == null || node.Waypoint == null) { continue; } @@ -1572,8 +1683,13 @@ namespace Barotrauma { connectedWp.isObstructed = true; wp.isObstructed = true; - obstructedNodes.Add(node); - obstructedNodes.Add(connection); + if (!obstructedNodes.TryGetValue(otherSub, out HashSet nodes)) + { + nodes = new HashSet(); + obstructedNodes.Add(otherSub, nodes); + } + nodes.Add(node); + nodes.Add(connection); break; } } @@ -1584,13 +1700,14 @@ namespace Barotrauma /// /// Only affects temporarily disabled waypoints. /// - public void EnableObstructedWaypoints() + public void EnableObstructedWaypoints(Submarine otherSub) { - foreach (var node in obstructedNodes) + if (obstructedNodes.TryGetValue(otherSub, out HashSet nodes)) { - node.Waypoint.isObstructed = false; + nodes.ForEach(n => n.Waypoint.isObstructed = false); + nodes.Clear(); + obstructedNodes.Remove(otherSub); } - obstructedNodes.Clear(); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs index c3abe40e2..149fa1fad 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs @@ -23,7 +23,6 @@ namespace Barotrauma const float VerticalDrag = 0.05f; const float MaxDrag = 0.1f; - public const float DamageDepth = -30000.0f; private const float ImpactDamageMultiplier = 10.0f; //limbs with a mass smaller than this won't cause an impact when they hit the sub @@ -40,7 +39,7 @@ namespace Barotrauma private set; } - private float depthDamageTimer; + private float depthDamageTimer = 10.0f; private readonly Submarine submarine; @@ -94,11 +93,6 @@ namespace Barotrauma get { return positionBuffer; } } - public bool AtDamageDepth - { - get { return Position.Y < DamageDepth; } - } - public Submarine Submarine { get { return submarine; } @@ -275,7 +269,7 @@ namespace Barotrauma if (impact.Target.UserData is VoronoiCell cell) { - HandleLevelCollision(impact); + HandleLevelCollision(impact, cell); } else if (impact.Target.Body.UserData is Structure) { @@ -451,29 +445,26 @@ namespace Barotrauma private void UpdateDepthDamage(float deltaTime) { - if (Position.Y > DamageDepth) { return; } #if CLIENT - if (GameMain.GameSession.GameMode is TestGameMode) { return; } + if (GameMain.GameSession?.GameMode is TestGameMode) { return; } #endif - float depth = DamageDepth - Position.Y; + if (Level.Loaded == null) { return; } + float submarineDepth = submarine.RealWorldDepth; + if (submarineDepth < Level.Loaded.RealWorldCrushDepth) { return; } depthDamageTimer -= deltaTime; - if (depthDamageTimer > 0.0f) { return; } foreach (Structure wall in Structure.WallList) { - if (wall.Submarine != submarine) { continue; } + if (wall.Submarine != submarine || wall.CrushDepth > submarineDepth) { continue; } - if (wall.Health < depth * 0.01f) + float pastCrushDepth = submarineDepth - wall.CrushDepth; + Explosion.RangedStructureDamage(wall.WorldPosition, 100.0f, pastCrushDepth * 0.1f, levelWallDamage: 0.0f); + if (Character.Controlled != null && Character.Controlled.Submarine == submarine) { - Explosion.RangedStructureDamage(wall.WorldPosition, 100.0f, depth * 0.01f); - - if (Character.Controlled != null && Character.Controlled.Submarine == submarine) - { - GameMain.GameScreen.Cam.Shake = Math.Max(GameMain.GameScreen.Cam.Shake, Math.Min(depth * 0.001f, 50.0f)); - } - } + GameMain.GameScreen.Cam.Shake = Math.Max(GameMain.GameScreen.Cam.Shake, Math.Min(pastCrushDepth * 0.001f, 50.0f)); + } } depthDamageTimer = 10.0f; @@ -655,7 +646,7 @@ namespace Barotrauma } } - private void HandleLevelCollision(Impact impact) + private void HandleLevelCollision(Impact impact, VoronoiCell cell = null) { if (GameMain.GameSession != null && Timing.TotalTime < GameMain.GameSession.RoundStartTime + 10) { @@ -672,6 +663,22 @@ namespace Barotrauma dockedSub.SubBody.ApplyImpact(wallImpact, -impact.Normal, impact.ImpactPos); } + 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) + { + var damagedStructures = Explosion.RangedStructureDamage( + ConvertUnits.ToDisplayUnits(impact.ImpactPos), + 500.0f, + hitWall.WallDamageOnTouch, + levelWallDamage: 0.0f); +#if CLIENT + PlayDamageSounds(damagedStructures, impact.ImpactPos, wallImpact, "StructureSlash"); +#endif + } + } + #if CLIENT int particleAmount = (int)Math.Min(wallImpact * 10.0f, 50); for (int i = 0; i < particleAmount; i++) @@ -836,34 +843,15 @@ 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 - //play a damage sound for the structure that took the most damage - float maxDamage = 0.0f; - Structure maxDamageStructure = null; - foreach (KeyValuePair structureDamage in damagedStructures) - { - if (maxDamageStructure == null || structureDamage.Value > maxDamage) - { - maxDamage = structureDamage.Value; - maxDamageStructure = structureDamage.Key; - } - } - - if (maxDamageStructure != null) - { - SoundPlayer.PlayDamageSound( - "StructureBlunt", - impact * 10.0f, - ConvertUnits.ToDisplayUnits(impactPos), - MathHelper.Lerp(2000.0f, 10000.0f, (impact - MinCollisionImpact) / 2.0f), - maxDamageStructure.Tags); - } + PlayDamageSounds(damagedStructures, impactPos, impact, "StructureBlunt"); #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs index 03a22bc07..783ee7440 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs @@ -23,7 +23,7 @@ namespace Barotrauma HideInMenus = 2 } - public enum SubmarineType { Player, Outpost, OutpostModule, Wreck } + public enum SubmarineType { Player, Outpost, OutpostModule, Wreck, BeaconStation } public enum SubmarineClass { Undefined, Scout, Attack, Transport, DeepDiver } partial class SubmarineInfo : IDisposable @@ -517,7 +517,8 @@ namespace Barotrauma { var contentPackageSubs = ContentPackage.GetFilesOfType( GameMain.Config.AllEnabledPackages, - ContentType.Submarine, ContentType.Outpost, ContentType.OutpostModule, ContentType.Wreck); + ContentType.Submarine, ContentType.Outpost, ContentType.OutpostModule, + ContentType.Wreck, ContentType.BeaconStation); for (int i = savedSubmarines.Count - 1; i >= 0; i--) { @@ -553,7 +554,7 @@ namespace Barotrauma subDirectories = Directory.GetDirectories(SavePath).Where(s => { DirectoryInfo dir = new DirectoryInfo(s); - return (dir.Attributes & System.IO.FileAttributes.Hidden) == 0; + return !dir.Attributes.HasFlag(System.IO.FileAttributes.Hidden) && !dir.Name.StartsWith("."); }).ToArray(); } catch (Exception e) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs index 713775336..ab9077d2e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs @@ -44,6 +44,8 @@ namespace Barotrauma public Hull CurrentHull { get; private set; } + public Level.Tunnel Tunnel; + public SpawnType SpawnType { get { return spawnType; } @@ -105,8 +107,8 @@ namespace Barotrauma { } - public WayPoint(MapEntityPrefab prefab, Rectangle newRect, Submarine submarine) - : base (prefab, submarine) + public WayPoint(MapEntityPrefab prefab, Rectangle newRect, Submarine submarine, ushort id = Entity.NullEntityID) + : base (prefab, submarine, id) { rect = newRect; idCardTags = new string[0]; @@ -626,7 +628,7 @@ namespace Barotrauma } } - public static WayPoint Load(XElement element, Submarine submarine) + public static WayPoint Load(XElement element, Submarine submarine, IdRemap idRemap) { Rectangle rect = new Rectangle( int.Parse(element.Attribute("x").Value), @@ -635,11 +637,7 @@ namespace Barotrauma Enum.TryParse(element.GetAttributeString("spawn", "Path"), out SpawnType spawnType); - WayPoint w = new WayPoint(MapEntityPrefab.Find(null, spawnType == SpawnType.Path ? "waypoint" : "spawnpoint"), rect, submarine) - { - ID = (ushort)int.Parse(element.Attribute("ID").Value) - }; - w.OriginalID = w.ID; + WayPoint w = new WayPoint(MapEntityPrefab.Find(null, spawnType == SpawnType.Path ? "waypoint" : "spawnpoint"), rect, submarine, idRemap.GetOffsetId(element)); w.spawnType = spawnType; string idCardDescString = element.GetAttributeString("idcarddesc", ""); @@ -663,14 +661,24 @@ namespace Barotrauma JobPrefab.Prefabs.Find(jp => jp.Name.Equals(jobIdentifier, StringComparison.OrdinalIgnoreCase)); } - w.ladderId = (ushort)element.GetAttributeInt("ladders", 0); - w.gapId = (ushort)element.GetAttributeInt("gap", 0); - w.linkedToID = new List(); + w.ladderId = idRemap.GetOffsetId(element.GetAttributeInt("ladders", 0)); + w.gapId = idRemap.GetOffsetId(element.GetAttributeInt("gap", 0)); + int i = 0; while (element.Attribute("linkedto" + i) != null) { - w.linkedToID.Add((ushort)int.Parse(element.Attribute("linkedto" + i).Value)); + int srcId = int.Parse(element.Attribute("linkedto" + i).Value); + int destId = idRemap.GetOffsetId(srcId); + if (destId > 0) + { + w.linkedToID.Add((ushort)destId); + } + else + { + w.unresolvedLinkedToID ??= new List(); + w.unresolvedLinkedToID.Add((ushort)srcId); + } i += 1; } return w; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs index 26058cca5..eaf19d174 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs @@ -26,6 +26,8 @@ namespace Barotrauma public readonly Submarine Submarine; public readonly float Condition; + public bool SpawnIfInventoryFull = true; + private readonly Action onSpawned; public ItemSpawnInfo(ItemPrefab prefab, Vector2 worldPosition, Action onSpawned, float? condition = null) @@ -62,6 +64,10 @@ namespace Barotrauma Item spawnedItem; if (Inventory?.Owner != null) { + if (!SpawnIfInventoryFull && !Inventory.Items.Any(it => it == null)) + { + return null; + } spawnedItem = new Item(Prefab, Vector2.Zero, null); if (!Inventory.Owner.Removed && !Inventory.TryPutItem(spawnedItem, null, spawnedItem.AllowedSlots)) { @@ -123,6 +129,36 @@ namespace Barotrauma } } + class SubmarineSpawnInfo : IEntitySpawnInfo + { + public readonly string Name; + + public readonly Vector2 Position; + + private readonly Action onSpawned; + + public SubmarineSpawnInfo(string name, Vector2 worldPosition, Action onSpawn = null) + { + this.Name = name ?? throw new ArgumentException("ItemSpawnInfo prefab cannot be null."); + Position = worldPosition; + this.onSpawned = onSpawn; + } + + + public Entity Spawn() + { + var submarine = string.IsNullOrEmpty(Name) ? null : + new Submarine(SubmarineInfo.SavedSubmarines.First(s => s.Name.Equals(Name, StringComparison.OrdinalIgnoreCase))); + return submarine; + } + + public void OnSpawned(Entity spawnedCharacter) + { + if (!(spawnedCharacter is Character character)) { throw new ArgumentException($"The entity passed to CharacterSpawnInfo.OnSpawned must be a Character (value was {spawnedCharacter?.ToString() ?? "null"})."); } + onSpawned?.Invoke(character); + } + } + private readonly Queue spawnQueue; private readonly Queue removeQueue; @@ -136,6 +172,14 @@ namespace Barotrauma public readonly bool Remove = false; + public override string ToString() + { + return + (Remove ? "Remove" : "Spawn") + "(" + + ((Entity as MapEntity)?.Name ?? "[NULL]") + + $", {OriginalID}, {OriginalInventoryID})"; + } + public SpawnOrRemove(Entity entity, bool remove) { Entity = entity; @@ -163,7 +207,7 @@ namespace Barotrauma } public EntitySpawner() - : base(null) + : base(null, Entity.EntitySpawnerID) { spawnQueue = new Queue(); removeQueue = new Queue(); @@ -200,7 +244,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) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } if (itemPrefab == null) @@ -210,7 +254,7 @@ 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 }); } public void AddToSpawnQueue(string speciesName, Vector2 worldPosition, Action onSpawn = null) @@ -253,7 +297,7 @@ namespace Barotrauma if (client != null) GameMain.Server.SetClientCharacter(client, null); } #endif - } + } removeQueue.Enqueue(entity); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/KarmaManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/KarmaManager.cs index b8688b981..f4b0eb0b1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/KarmaManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/KarmaManager.cs @@ -32,8 +32,10 @@ namespace Barotrauma [Serialize(0.05f, true)] public float StructureRepairKarmaIncrease { get; set; } + [Serialize(0.1f, true)] public float StructureDamageKarmaDecrease { get; set; } + [Serialize(30.0f, true)] public float MaxStructureDamageKarmaDecreasePerSecond { get; set; } @@ -67,6 +69,9 @@ namespace Barotrauma [Serialize(defaultValue: false, true)] public bool DangerousItemStealBots { get; set; } + [Serialize(defaultValue: 0.05f, true)] + public float BallastFloraKarmaIncrease { get; set; } + private int allowedWireDisconnectionsPerMinute; [Serialize(5, true)] diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEventManager.cs index 2b69aea0c..2f9138c36 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEventManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; namespace Barotrauma.Networking { @@ -42,21 +43,6 @@ 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 + ")"); - 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) { @@ -65,7 +51,7 @@ namespace Barotrauma.Networking } 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 766e1d8e1..864233654 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs @@ -29,7 +29,8 @@ namespace Barotrauma.Networking REQUEST_STARTGAMEFINALIZE, //tell the server you're ready to finalize round initialization ERROR, //tell the server that an error occurred - CREW + CREW, + READY_CHECK } enum ClientNetObject @@ -78,7 +79,8 @@ namespace Barotrauma.Networking MISSION, EVENTACTION, RESET_UPGRADES, //inform the clients that the upgrades on the submarine have been reset - CREW //anything related to managing bots in multiplayer + CREW, //anything related to managing bots in multiplayer + READY_CHECK //start, end and update a ready check } enum ServerNetObject { @@ -113,6 +115,13 @@ namespace Barotrauma.Networking SwitchSub } + public enum ReadyCheckState + { + Start, + Update, + End + } + enum DisconnectReason { Unknown, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs index 2451008d5..acee8540e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs @@ -13,6 +13,11 @@ //additional instructions (power up, fire at will, etc) public readonly string OrderOption; + /// + /// 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, order?.GetChatMessage(targetCharacter?.Name, sender?.CurrentHull?.DisplayName, givingOrderToSelf: targetCharacter == sender, orderOption: orderOption), 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 230398ede..0ac054cdc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs @@ -62,7 +62,7 @@ namespace Barotrauma.Networking public Submarine RespawnShuttle { get; private set; } public RespawnManager(NetworkMember networkMember, SubmarineInfo shuttleInfo) - : base(null) + : base(null, Entity.RespawnManagerID) { this.networkMember = networkMember; @@ -265,7 +265,7 @@ namespace Barotrauma.Networking #endif c.Kill(CauseOfDeathType.Unknown, null, true); c.Enabled = false; - + Spawner.AddToRemoveQueue(c); if (c.Inventory != null) { 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/Physics/Physics.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/Physics.cs index 2c32ebdff..79e8f0e01 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/Physics.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/Physics.cs @@ -1,4 +1,5 @@ using FarseerPhysics.Dynamics; +using Microsoft.Xna.Framework; namespace Barotrauma { @@ -19,7 +20,7 @@ namespace Barotrauma public static float DisplayToRealWorldRatio = 1.0f / 100.0f; public const float DisplayToSimRation = 100.0f; - + public static bool TryParseCollisionCategory(string categoryName, out Category category) { category = Category.None; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs index 1fd780dcd..540958266 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs @@ -254,6 +254,8 @@ namespace Barotrauma /// public float TransformedRotation => TransformRotation(Rotation, Dir); + public float TransformRotation(float rotation) => TransformRotation(rotation, dir); + public static float TransformRotation(float rot, float dir) => dir < 0 ? rot - MathHelper.Pi : rot; public Vector2 LinearVelocity @@ -711,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; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs index b252704ce..62839c061 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs @@ -88,35 +88,30 @@ namespace Barotrauma DebugConsole.ThrowError($"Prefab \"{prefab.OriginalName}\" has no identifier!"); } - List newList = null; - if (!prefabs.TryGetValue(prefab.Identifier, out List list)) + bool basePrefabExists = prefabs.TryGetValue(prefab.Identifier, out List list); + + //Handle bad overrides and duplicates + if (basePrefabExists && !isOverride) { - newList = new List(); newList.Add(null); - list = newList; + DebugConsole.ThrowError($"Error registering \"{prefab.OriginalName}\", \"{prefab.Identifier}\" ({typeof(T).ToString()}): base already exists; try overriding"); + return; } - if (isOverride) + + //Add to list + if (!basePrefabExists) { - /*if (list[0] == null) - { - DebugConsole.ThrowError($"Error registering \"{prefab.OriginalName}\", \"{prefab.Identifier}\" ({typeof(T).ToString()}): overriding when base doesn't exist"); - return; - }*/ - list.Add(prefab); - } - else - { - if (list[0] != null) - { - DebugConsole.ThrowError($"Error registering \"{prefab.OriginalName}\", \"{prefab.Identifier}\" ({typeof(T).ToString()}): base already exists; try overriding"); - return; - } - list[0] = prefab; + list = new List(); } + list.Add(prefab); + Sort(list); - if (newList != null) { prefabs.Add(prefab.Identifier, newList); } + if (!basePrefabExists) + { + prefabs.Add(prefab.Identifier, list); + } } /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs b/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs index ec801f1f3..feeb54020 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs @@ -135,7 +135,7 @@ namespace Voronoi2 public enum CellType { - Solid, Empty, Edge, Path, Removed + Solid, Empty, Path, Removed } public class VoronoiCell @@ -151,6 +151,11 @@ namespace Voronoi2 public Vector2 Translation; + 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; } @@ -204,8 +209,8 @@ namespace Voronoi2 public VoronoiCell Cell1, Cell2; public bool IsSolid; - public bool OutsideLevel; + public bool NextToCave, NextToMainPath, NextToSidePath; public Vector2 Center { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs index 6f02eb491..fb736fba5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs @@ -119,11 +119,15 @@ namespace Barotrauma } #endif +#if CLIENT + GameMain.LightManager?.Update((float)deltaTime); +#endif + GameTime += deltaTime; foreach (PhysicsBody body in PhysicsBody.List) { - if (body.Enabled) { body.Update(); } + if (body.Enabled && body.BodyType != FarseerPhysics.BodyType.Static) { body.Update(); } } foreach (MapEntity e in MapEntity.mapEntityList) { @@ -221,7 +225,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/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index feacc748a..cefc33983 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) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs index 67069c0ee..ac1f25a7a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs @@ -31,18 +31,22 @@ namespace Barotrauma { public static readonly List DelayList = new List(); - private enum DelayTypes { timer = 0, reachcursor = 1 } + private enum DelayTypes { Timer = 0, ReachCursor = 1 } - private DelayTypes delayType; - private float delay; + private readonly DelayTypes delayType; + private readonly float delay; public DelayedEffect(XElement element, string parentDebugName) : base(element, parentDebugName) { - delayType = (DelayTypes)Enum.Parse(typeof(DelayTypes), element.GetAttributeString("delaytype", "timer")); + string delayTypeStr = element.GetAttributeString("delaytype", "timer"); + if (!Enum.TryParse(typeof(DelayTypes), delayTypeStr, ignoreCase: true, out var delayType)) + { + DebugConsole.ThrowError("Invalid delay type \"" + delayTypeStr + "\" in StatusEffect (" + parentDebugName + ")"); + } switch (delayType) { - case DelayTypes.timer: + case DelayTypes.Timer: delay = element.GetAttributeFloat("delay", 1.0f); break; } @@ -57,10 +61,10 @@ namespace Barotrauma switch (delayType) { - case DelayTypes.timer: + case DelayTypes.Timer: DelayList.Add(new DelayedListElement(this, entity, target.ToEnumerable(), delay, worldPosition, null)); break; - case DelayTypes.reachcursor: + case DelayTypes.ReachCursor: Projectile projectile = (entity as Item)?.GetComponent(); if (projectile == null) { @@ -83,7 +87,7 @@ 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) @@ -100,10 +104,10 @@ namespace Barotrauma switch (delayType) { - case DelayTypes.timer: + case DelayTypes.Timer: DelayList.Add(new DelayedListElement(this, entity, targets, delay, worldPosition, null)); break; - case DelayTypes.reachcursor: + case DelayTypes.ReachCursor: Projectile projectile = (entity as Item)?.GetComponent(); if (projectile == null) { @@ -139,12 +143,12 @@ namespace Barotrauma switch (element.Parent.delayType) { - case DelayTypes.timer: + case DelayTypes.Timer: element.Delay -= deltaTime; if (element.Delay > 0.0f) { continue; } break; - case DelayTypes.reachcursor: - if (Vector2.Distance(element.Entity.WorldPosition, element.StartPosition.Value) < element.Delay) continue; + case DelayTypes.ReachCursor: + if (Vector2.Distance(element.Entity.WorldPosition, element.StartPosition.Value) < element.Delay) { continue; } break; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index 72e556013..acf79c149 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -64,12 +64,23 @@ namespace Barotrauma ContainedInventory } + public enum SpawnRotationType + { + Fixed, + Target, + Limb, + MainLimb, + Collider + } + public readonly ItemPrefab ItemPrefab; public readonly SpawnPositionType SpawnPosition; public readonly float Speed; public readonly float Rotation; public readonly int Count; public readonly float Spread; + public readonly SpawnRotationType RotationType; + public readonly float AimSpread; public ItemSpawnInfo(XElement element, string parentDebugName) { @@ -101,15 +112,22 @@ namespace Barotrauma } Speed = element.GetAttributeFloat("speed", 0.0f); + Rotation = element.GetAttributeFloat("rotation", 0.0f); Count = element.GetAttributeInt("count", 1); Spread = element.GetAttributeFloat("spread", 0f); + AimSpread = element.GetAttributeFloat("aimspread", 0f); string spawnTypeStr = element.GetAttributeString("spawnposition", "This"); - if (!Enum.TryParse(spawnTypeStr, out SpawnPosition)) + if (!Enum.TryParse(spawnTypeStr, ignoreCase: true, out SpawnPosition)) { DebugConsole.ThrowError("Error in StatusEffect config - \"" + spawnTypeStr + "\" is not a valid spawn position."); } + string rotationTypeStr = element.GetAttributeString("rotationtype", Rotation != 0 ? "Fixed" : "Target"); + if (!Enum.TryParse(rotationTypeStr, ignoreCase: true, out RotationType)) + { + DebugConsole.ThrowError("Error in StatusEffect config - \"" + rotationTypeStr + "\" is not a valid rotation type."); + } } } @@ -146,7 +164,7 @@ namespace Barotrauma private readonly List requiredItems; public readonly string[] propertyNames; - private readonly object[] propertyEffects; + public readonly object[] propertyEffects; private readonly PropertyConditional.Comparison conditionalComparison = PropertyConditional.Comparison.Or; private readonly List propertyConditionals; @@ -168,9 +186,14 @@ namespace Barotrauma public readonly bool Stackable = true; //Can the same status effect be applied several times to the same targets? +#if CLIENT + private readonly bool playSoundOnRequiredItemFailure = false; +#endif + private readonly int useItemCount; - private readonly bool removeItem, removeCharacter; + private readonly bool removeItem, removeCharacter, breakLimb, hideLimb; + private readonly float hideLimbTimer; public readonly ActionType type = ActionType.OnActive; @@ -179,6 +202,9 @@ namespace Barotrauma private readonly List spawnItems; private readonly List spawnCharacters; + private List triggeredEvents; + private string triggeredEventTag = "statuseffect"; + private Character user; public readonly float FireSize; @@ -187,6 +213,11 @@ namespace Barotrauma public readonly float SeverLimbsProbability; + public PhysicsBody sourceBody; + + public readonly bool OnlyInside; + public readonly bool OnlyOutside; + public HashSet TargetIdentifiers { get { return targetIdentifiers; } @@ -248,8 +279,11 @@ namespace Barotrauma spawnCharacters = new List(); Afflictions = 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); @@ -268,6 +302,21 @@ namespace Barotrauma List propertyAttributes = new List(); propertyConditionals = new List(); + string[] targetTypesStr = + element.GetAttributeStringArray("target", null) ?? + element.GetAttributeStringArray("targettype", new string[0]); + foreach (string s in targetTypesStr) + { + if (!Enum.TryParse(s, true, out TargetType targetType)) + { + DebugConsole.ThrowError("Invalid target type \"" + s + "\" in StatusEffect (" + parentDebugName + ")"); + } + else + { + targetTypes |= targetType; + } + } + foreach (XAttribute attribute in attributes) { switch (attribute.Name.ToString()) @@ -280,18 +329,6 @@ namespace Barotrauma break; case "targettype": case "target": - string[] Flags = attribute.Value.Split(','); - foreach (string s in Flags) - { - if (!Enum.TryParse(s, true, out TargetType targetType)) - { - DebugConsole.ThrowError("Invalid target type \"" + s + "\" in StatusEffect (" + parentDebugName + ")"); - } - else - { - targetTypes |= targetType; - } - } break; case "disabledeltatime": disableDeltaTime = attribute.GetAttributeBool(false); @@ -324,6 +361,9 @@ namespace Barotrauma lifeTime = attribute.GetAttributeFloat(0); lifeTimer = lifeTime; break; + case "eventtag": + triggeredEventTag = attribute.Value; + break; case "checkconditionalalways": CheckConditionalAlways = attribute.GetAttributeBool(false); break; @@ -334,10 +374,23 @@ namespace Barotrauma DebugConsole.ThrowError("Invalid conditional comparison type \"" + attribute.Value + "\" in StatusEffect (" + parentDebugName + ")"); } break; +#if CLIENT + case "playsoundonrequireditemfailure": + playSoundOnRequiredItemFailure = attribute.GetAttributeBool(false); + break; +#endif case "sound": DebugConsole.ThrowError("Error in StatusEffect " + element.Parent.Name.ToString() + " - sounds should be defined as child elements of the StatusEffect, not as attributes."); break; + case "delay": + break; + case "range": + if (!HasTargetType(TargetType.NearbyCharacters) && !HasTargetType(TargetType.NearbyItems)) + { + propertyAttributes.Add(attribute); + } + break; default: propertyAttributes.Add(attribute); break; @@ -377,6 +430,13 @@ namespace Barotrauma case "removecharacter": removeCharacter = true; break; + case "breaklimb": + breakLimb = true; + break; + case "hidelimb": + hideLimb = true; + hideLimbTimer = subElement.GetAttributeFloat("duration", 0); + break; case "requireditem": case "requireditems": RelatedItem newRequiredItem = RelatedItem.Load(subElement, returnEmpty: false, parentDebugName: parentDebugName); @@ -454,6 +514,17 @@ 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); + } + } + break; case "spawncharacter": var newSpawnCharacter = new CharacterSpawnInfo(subElement, parentDebugName); if (!string.IsNullOrWhiteSpace(newSpawnCharacter.SpeciesName)) { spawnCharacters.Add(newSpawnCharacter); } @@ -507,8 +578,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); } @@ -516,12 +586,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.Add(item); + 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); + } } } } @@ -616,15 +701,11 @@ namespace Barotrauma 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) { @@ -633,13 +714,40 @@ namespace Barotrauma } 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); } 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.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; @@ -688,7 +796,17 @@ namespace Barotrauma if (targetIdentifiers != null && currentTargets.Count == 0) { return; } - if (!HasRequiredItems(entity) || !HasRequiredConditions(currentTargets)) { return; } + bool hasRequiredItems = HasRequiredItems(entity); + if (!hasRequiredItems || !HasRequiredConditions(currentTargets)) + { +#if CLIENT + if (!hasRequiredItems && playSoundOnRequiredItemFailure) + { + PlaySound(entity, GetHull(entity), GetPosition(entity, targets, worldPosition)); + } +#endif + return; + } if (duration > 0.0f && !Stackable) { @@ -723,9 +841,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; @@ -733,8 +851,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; } @@ -790,6 +907,27 @@ namespace Barotrauma if (target is Character character) { Entity.Spawner?.AddToRemoveQueue(character); } } } + if (breakLimb || hideLimb) + { + foreach (var target in targets) + { + if (target is Character character) + { + var matchingLimb = character.AnimController.Limbs.FirstOrDefault(l => l.body == sourceBody); + if (matchingLimb != null) + { + if (breakLimb) + { + character.TrySeverLimbJoints(matchingLimb, severLimbsProbability: 100, damage: 100, allowBeheading: true); + } + else + { + matchingLimb.HideAndDisable(hideLimbTimer); + } + } + } + } + } if (duration > 0.0f) { @@ -902,6 +1040,25 @@ 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 && !string.IsNullOrWhiteSpace(triggeredEventTag)) + { + List eventTargets = targets.Where(t => t is Entity).Cast().ToList(); + + if (eventTargets.Any()) + { + scriptedEvent.Targets.Add(triggeredEventTag, eventTargets); + } + } + } + } + if (isNotClient && entity != null && Entity.Spawner != null) //clients are not allowed to spawn entities { foreach (CharacterSpawnInfo characterSpawnInfo in spawnCharacters) @@ -936,8 +1093,44 @@ namespace Barotrauma case ItemSpawnInfo.SpawnPositionType.This: Entity.Spawner.AddToSpawnQueue(itemSpawnInfo.ItemPrefab, position + Rand.Vector(itemSpawnInfo.Spread, Rand.RandSync.Server), onSpawned: newItem => { - newItem.body?.ApplyLinearImpulse(Rand.Vector(1) * itemSpawnInfo.Speed); - newItem.Rotation = itemSpawnInfo.Rotation; + Projectile projectile = newItem.GetComponent(); + if (projectile != null && user != null && sourceBody != null && entity != null) + { + float spread = MathHelper.ToRadians(Rand.Range(-itemSpawnInfo.AimSpread, itemSpawnInfo.AimSpread)); + var worldPos = sourceBody.Position; + float rotation = itemSpawnInfo.Rotation; + if (user.Submarine != null) + { + worldPos += user.Submarine.Position; + } + switch (itemSpawnInfo.RotationType) + { + case ItemSpawnInfo.SpawnRotationType.Fixed: + rotation = sourceBody.TransformRotation(itemSpawnInfo.Rotation); + break; + case ItemSpawnInfo.SpawnRotationType.Target: + rotation = MathUtils.VectorToAngle(entity.WorldPosition - worldPos); + break; + case ItemSpawnInfo.SpawnRotationType.Limb: + rotation = sourceBody.TransformedRotation; + break; + case ItemSpawnInfo.SpawnRotationType.Collider: + rotation = user.AnimController.Collider.Rotation; + break; + case ItemSpawnInfo.SpawnRotationType.MainLimb: + rotation = user.AnimController.MainLimb.body.TransformedRotation; + break; + 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); + } + else + { + newItem.body?.ApplyLinearImpulse(Rand.Vector(1) * itemSpawnInfo.Speed); + newItem.Rotation = itemSpawnInfo.Rotation; + } }); break; case ItemSpawnInfo.SpawnPositionType.ThisInventory: diff --git a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs index 7fa44d79e..61f6dc39f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs @@ -71,11 +71,11 @@ namespace Barotrauma //achievement for descending below crush depth and coming back if (Timing.TotalTime > GameMain.GameSession.RoundStartTime + 30.0f) { - if (c.WorldPosition.Y < SubmarineBody.DamageDepth || (c.Submarine != null && c.Submarine.WorldPosition.Y < SubmarineBody.DamageDepth)) + if (c.Submarine != null && c.Submarine.AtDamageDepth || Level.Loaded.GetRealWorldDepth(c.WorldPosition.Y) > Level.Loaded.RealWorldCrushDepth) { roundData.EnteredCrushDepth.Add(c); } - else if (c.WorldPosition.Y > SubmarineBody.DamageDepth * 0.5f) + else if (Level.Loaded.GetRealWorldDepth(c.WorldPosition.Y) < Level.Loaded.RealWorldCrushDepth * 0.5f) { //all characters that have entered crush depth and are still alive get an achievement if (roundData.EnteredCrushDepth.Contains(c)) UnlockAchievement(c, "survivecrushdepth"); 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..60c134407 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'."); @@ -143,7 +143,7 @@ namespace Barotrauma private bool Disposed { get; set; } - public bool SupressWarnings { get; } + public bool SuppressWarnings { get; } public bool HideInMenus { get; } @@ -159,7 +159,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 +219,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 +243,9 @@ namespace Barotrauma Prefabs.Add(this, isOverride); } - public static UpgradePrefab? Find(string idenfitier) + public static UpgradePrefab? Find(string identifier) { - return !string.IsNullOrWhiteSpace(idenfitier) ? Prefabs.Find(prefab => prefab.Identifier == idenfitier) : null; + 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 new file mode 100644 index 000000000..b1c8ab169 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/IdRemap.cs @@ -0,0 +1,103 @@ +using Microsoft.Xna.Framework; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + public class IdRemap + { + public static readonly IdRemap DiscardId = new IdRemap(null, -1); + + private int maxId; + + private List srcRanges; + private int destOffset; + + public IdRemap(XElement parentElement, int offset) + { + destOffset = offset; + if (parentElement != null) + { + srcRanges = new List(); + foreach (XElement subElement in parentElement.Elements()) + { + int id = subElement.GetAttributeInt("ID", -1); + if (id > 0) { InsertId(id); } + } + maxId = GetOffsetId(srcRanges.Last().Y + 1); + } + else + { + maxId = offset + 1; + } + } + + public ushort AssignMaxId() + { + maxId++; + return (ushort)maxId; + } + + private void InsertId(int id) + { + for (int i=0;i id) + { + if (srcRanges[i].X == (id + 1)) + { + srcRanges[i] = new Point(id, srcRanges[i].Y); + if (i > 0 && srcRanges[i].X == srcRanges[i - 1].Y) + { + srcRanges[i - 1] = new Point(srcRanges[i - 1].X, srcRanges[i].Y); + srcRanges.RemoveAt(i); + } + } + else + { + srcRanges.Insert(i, new Point(id, id)); + } + return; + } + else if (srcRanges[i].Y < id) + { + 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) + { + srcRanges[i] = new Point(srcRanges[i].X, srcRanges[i + 1].Y); + srcRanges.RemoveAt(i+1); + } + return; + } + } + } + srcRanges.Add(new Point(id, id)); + } + + public ushort GetOffsetId(XElement element) + { + return GetOffsetId(element.GetAttributeInt("ID", 0)); + } + + public ushort GetOffsetId(int id) + { + if (id <= 0) { return 0; } + if (destOffset < 0) { return 0; } + 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))) + { + return (ushort)(id - srcRanges[i].X + 1 + currOffset); + } + currOffset += srcRanges[i].Y - srcRanges[i].X + 1; + } + return 0; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs index 46ea97dbc..47c0cb08b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/MathUtils.cs @@ -13,6 +13,7 @@ namespace Barotrauma TopLeft = (Top | Left), TopCenter = (CenterX | Top), TopRight = (Top | Right), CenterLeft = (Left | CenterY), Center = (CenterX | CenterY), CenterRight = (Right | CenterY), BottomLeft = (Bottom | Left), BottomCenter = (CenterX | Bottom), BottomRight = (Bottom | Right), + Any = Left | Right | Top | Bottom | Center } static class MathUtils @@ -368,7 +369,7 @@ namespace Barotrauma public static bool GetLineRectangleIntersection(Vector2 a1, Vector2 a2, Rectangle rect, out Vector2 intersection) { - if (GetAxisAlignedLineIntersection(a1, a2, + if (GetAxisAlignedLineIntersection(a1, a2, new Vector2(rect.X, rect.Y), new Vector2(rect.Right, rect.Y), true, out intersection)) @@ -377,14 +378,14 @@ namespace Barotrauma } if (GetAxisAlignedLineIntersection(a1, a2, - new Vector2(rect.X, rect.Y-rect.Height), - new Vector2(rect.Right, rect.Y-rect.Height), + new Vector2(rect.X, rect.Y - rect.Height), + new Vector2(rect.Right, rect.Y - rect.Height), true, out intersection)) { return true; } - if(GetAxisAlignedLineIntersection(a1, a2, + if (GetAxisAlignedLineIntersection(a1, a2, new Vector2(rect.X, rect.Y), new Vector2(rect.X, rect.Y - rect.Height), false, out intersection)) @@ -548,8 +549,72 @@ namespace Barotrauma } float numerator = xDiff * (lineA.Y - point.Y) - yDiff * (lineA.X - point.X); - return (numerator*numerator) / - (xDiff * xDiff + yDiff * yDiff); + return (numerator * numerator) / (xDiff * xDiff + yDiff * yDiff); + } + + public static double LineSegmentToPointDistanceSquared(Point lineA, Point lineB, Point point) + { + double xDiff = lineB.X - lineA.X; + double yDiff = lineB.Y - lineA.Y; + + if (xDiff == 0 && yDiff == 0) + { + double v1 = lineA.X - point.X; + double v2 = lineA.Y - point.Y; + return (v1 * v1) + (v2 * v2); + } + + // Calculate the t that minimizes the distance. + double t = ((point.X - lineA.X) * xDiff + (point.Y - lineA.Y) * yDiff) / (xDiff * xDiff + yDiff * yDiff); + + // See if this represents one of the segment's + // end points or a point in the middle. + if (t < 0) + { + xDiff = point.X - lineA.X; + yDiff = point.Y - lineA.Y; + } + else if (t > 1) + { + xDiff = point.X - lineB.X; + yDiff = point.Y - lineB.Y; + } + else + { + xDiff = point.X - (lineA.X + t * xDiff); + yDiff = point.Y - (lineA.Y + t * yDiff); + } + + return xDiff * xDiff + yDiff * yDiff; + } + + public static Vector2 GetClosestPointOnLineSegment(Vector2 lineA, Vector2 lineB, Vector2 point) + { + float xDiff = lineB.X - lineA.X; + float yDiff = lineB.Y - lineA.Y; + + if (xDiff == 0 && yDiff == 0) + { + return lineA; + } + + // Calculate the t that minimizes the distance. + float t = ((point.X - lineA.X) * xDiff + (point.Y - lineA.Y) * yDiff) / (xDiff * xDiff + yDiff * yDiff); + + // See if this represents one of the segment's + // end points or a point in the middle. + if (t < 0) + { + return lineA; + } + else if (t > 1) + { + return lineB; + } + else + { + return new Vector2(lineA.X + t * xDiff, lineA.Y + t * yDiff); + } } public static bool CircleIntersectsRectangle(Vector2 circlePos, float radius, Rectangle rect) @@ -613,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; } @@ -658,7 +707,7 @@ namespace Barotrauma for (int i = 1; i < points.Count; i++) { - if (points[i] == currPoint) continue; + if (points[i].NearlyEquals(currPoint)) continue; if (currPoint == endPoint || MathUtils.VectorOrientation(currPoint, endPoint, points[i]) == -1) { diff --git a/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub b/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub index 065f88283..ea738a58e 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 ed6d8292a..fb6c5223f 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 db7711f8b..69cf3a60b 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..92d0a7813 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 1e07c93fa..176e8332c 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 0b1a4f5b7..3cd744805 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub and b/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Orca.sub b/Barotrauma/BarotraumaShared/Submarines/Orca.sub index c10d5de24..dea22be13 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 1fe91b36e..d7d09c724 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 12d7aac5c..5b2a755e7 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Remora.sub and b/Barotrauma/BarotraumaShared/Submarines/Remora.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Typhon.sub b/Barotrauma/BarotraumaShared/Submarines/Typhon.sub index a8377211d..3d60b8f12 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 ace353ab4..cf91783c4 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub and b/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub differ diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index d29dab26c..a3e6d57c1 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,193 @@ +--------------------------------------------------------------------------------------------------------- +v0.11.0.9 +--------------------------------------------------------------------------------------------------------- + +- 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. +- Added small explorable caves alongside the main path. +- Made the floating ice chunks destructible. +- The areas outside the traversable path are solid instead of hollow, so it's no longer possible for monsters to spawn "outside the level" or for the sub to get lost at the wrong side of the level walls. +- Ice spires: tall, protrusions that cut holes in the submarine if you hit them. +- 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. +- Made thermal artifacts a bit more manageable: they now start a fires periodically, not continuously. + +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. +- Level difficulty affects the way outpost events are chosen (the more difficult events don't occur until later in the campaign). +- Non-wiring-related items become transparent in the sub editor's wiring mode. +- 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. +- 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 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. 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 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. +- Fixed atan component output being inconsistent: the output was only correct if the y input was received after the x input during the same frame. +- Fixed crashing if a character's hands get severed while repairing something. +- Fixed stored non-shadow-casting lights being subtracted from the shadow-casting light count in the sub editor. +- Fixed autocalculated submarine price not being saved if the player doesn't touch the price field in the sub editor's save dialog. +- Fixed OnContained/OnNotContained StatusEffects not running if the itemcomponent is not active. In practice prevented oxygenite shards from supplying power until someone moves them. May have caused some other issues as well. +- Fixed console error when deconstruction Bufotoxin. +- 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 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. +- Fixed StatusEffects targeting "NearbyItems" only being able to target the Item instance, not the ItemComponents. +- Fixed "deceased" text wrapping in the health interface on small resolutions. +- Fixed open subinventories remaining visible on screen when using a railgun, coilgun or periscope. +- 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/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" diff --git a/Libraries/Facepunch.Steamworks/Structs/LobbyQuery.cs b/Libraries/Facepunch.Steamworks/Structs/LobbyQuery.cs index 384fede30..f17ecf865 100644 --- a/Libraries/Facepunch.Steamworks/Structs/LobbyQuery.cs +++ b/Libraries/Facepunch.Steamworks/Structs/LobbyQuery.cs @@ -219,6 +219,7 @@ namespace Steamworks.Data /// public async Task RequestAsync() { + await Task.Yield(); ApplyFilters(); LobbyMatchList_t? list = await SteamMatchmaking.Internal.RequestLobbyList(); diff --git a/Libraries/Farseer Physics Engine 3.5/Dynamics/Body.cs b/Libraries/Farseer Physics Engine 3.5/Dynamics/Body.cs index 88be2106d..0b9a0f7a5 100644 --- a/Libraries/Farseer Physics Engine 3.5/Dynamics/Body.cs +++ b/Libraries/Farseer Physics Engine 3.5/Dynamics/Body.cs @@ -468,6 +468,17 @@ namespace FarseerPhysics.Dynamics } } + private float gravityScale = 1.0f; + public float GravityScale + { + get { return gravityScale; } + set + { + if (!MathUtils.IsValid(value)) { return; } + gravityScale = value; + } + } + /// /// Gets or sets a value indicating whether this body ignores gravity. /// @@ -585,7 +596,7 @@ namespace FarseerPhysics.Dynamics /// Warning: This method is locked during callbacks. /// > /// Thrown when the world is Locked/Stepping. - public void Add(Fixture fixture) + public void Add(Fixture fixture, bool resetMassData = true) { if (World != null && World.IsLocked) throw new WorldLockedException("Cannot add fixtures to a body when the World is locked."); @@ -607,7 +618,7 @@ namespace FarseerPhysics.Dynamics #endif // Adjust mass properties if needed. - if (fixture.Shape._density > 0.0f) + if (fixture.Shape._density > 0.0f && resetMassData) ResetMassData(); if (World != null) @@ -1303,6 +1314,7 @@ namespace FarseerPhysics.Dynamics body.IsBullet = IsBullet; body.IgnoreCCD = IgnoreCCD; body.IgnoreGravity = IgnoreGravity; + body.gravityScale = gravityScale; body._torque = _torque; return body; diff --git a/Libraries/Farseer Physics Engine 3.5/Dynamics/Island.cs b/Libraries/Farseer Physics Engine 3.5/Dynamics/Island.cs index 9ef3fb8cd..798c4dc4f 100644 --- a/Libraries/Farseer Physics Engine 3.5/Dynamics/Island.cs +++ b/Libraries/Farseer Physics Engine 3.5/Dynamics/Island.cs @@ -126,7 +126,7 @@ namespace FarseerPhysics.Dynamics if (b.IgnoreGravity) v += h * (b._invMass * b._force); else - v += h * (gravity + b._invMass * b._force); + v += h * (gravity * b.GravityScale + b._invMass * b._force); w += h * b._invI * b._torque; diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/Texture.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/Texture.cs index 95a62e646..4a00e6dad 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/Texture.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/Texture.cs @@ -24,7 +24,7 @@ namespace Microsoft.Xna.Framework.Graphics /// The value is an implementation detail and may change between application launches or MonoGame versions. /// It is only guaranteed to stay consistent during application lifetime. /// - internal int SortingKey + public int SortingKey { get { return _sortingKey; } }