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/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index 9a94fef2e..5dc9cab53 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -580,14 +580,23 @@ namespace Barotrauma soundTimer -= deltaTime; } else if (AIController != null) - { + { switch (AIController.State) { case AIState.Attack: PlaySound(CharacterSound.SoundType.Attack); break; default: - PlaySound(CharacterSound.SoundType.Idle); + var petBehavior = (AIController as EnemyAIController)?.PetBehavior; + if (petBehavior != null && petBehavior.Happiness < petBehavior.MaxHappiness * 0.25f) + { + PlaySound(CharacterSound.SoundType.Unhappy); + } + else + { + PlaySound(CharacterSound.SoundType.Idle); + + } break; } } @@ -801,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; @@ -826,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(); @@ -877,11 +886,12 @@ namespace Barotrauma private readonly List matchingSounds = new List(); private SoundChannel soundChannel; - public void PlaySound(CharacterSound.SoundType soundType) + public void PlaySound(CharacterSound.SoundType soundType, float soundIntervalFactor = 1.0f) { if (sounds == null || sounds.Count == 0) { return; } if (soundChannel != null && soundChannel.IsPlaying) { return; } if (GameMain.SoundManager?.Disabled ?? true) { return; } + if (soundTimer > soundInterval * soundIntervalFactor) { return; } matchingSounds.Clear(); foreach (var s in sounds) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index 23cab333e..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) @@ -426,7 +444,7 @@ namespace Barotrauma Vector2 startPos = character.DrawPosition + (character.FocusedCharacter.DrawPosition - character.DrawPosition) * 0.7f; startPos = cam.WorldToScreen(startPos); - string focusName = character.FocusedCharacter.DisplayName; + string focusName = character.FocusedCharacter.Info == null ? character.FocusedCharacter.DisplayName : character.FocusedCharacter.Info.DisplayName; Vector2 textPos = startPos; Vector2 textSize = GUI.Font.MeasureString(focusName); Vector2 largeTextSize = GUI.SubHeadingFont.MeasureString(focusName); @@ -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 3a23032b7..e02363bf8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -84,6 +84,10 @@ namespace Barotrauma { newMem.interact = FocusedCharacter.ID; } + else if (newMem.states.HasFlag(InputNetFlags.Use) && (FocusedCharacter?.IsPet ?? false)) + { + newMem.interact = FocusedCharacter.ID; + } else if (focusedItem != null && !CharacterInventory.DraggingItemToWorld && !newMem.states.HasFlag(InputNetFlags.Grab) && !newMem.states.HasFlag(InputNetFlags.Health)) { @@ -254,6 +258,7 @@ namespace Barotrauma if (readStatus) { ReadStatus(msg); + (AIController as EnemyAIController)?.PetBehavior?.ClientRead(msg); } msg.ReadPadBits(); @@ -411,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) { @@ -429,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/CharacterSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterSound.cs index e93c6e2b8..c247da8e5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterSound.cs @@ -6,7 +6,7 @@ namespace Barotrauma { public enum SoundType { - Idle, Attack, Die, Damage + Idle, Attack, Die, Damage, Happy, Unhappy } private readonly RoundSound roundSound; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index d797cb4b4..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,7 +225,16 @@ namespace Barotrauma Character.Controlled.ResetInteract = true; if (openHealthWindow != null) { - openHealthWindow.characterName.Text = value.Character.Name; + 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) { Character.Controlled.SelectedConstruction = null; @@ -322,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); @@ -397,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) { @@ -414,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); }); @@ -943,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..fb33016be 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) => { 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/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index 4090a12e9..5efb2a598 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; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index 72234cabf..5ad4e99e6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -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, @@ -2255,29 +2319,33 @@ namespace Barotrauma 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 @@ -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 01df20fc1..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 @@ -712,7 +717,7 @@ namespace Barotrauma } } - XElement petsElement = new XElement("pets"); + petsElement = new XElement("pets"); PetBehavior.SavePets(petsElement); modeElement.Add(petsElement); 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..5894eb5fb --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs @@ -0,0 +1,266 @@ +#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, status) 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) { return; } + + switch (state) + { + case ReadyCheckState.Start: + bool isOwn = false; + + float duration = inc.ReadSingle(); + string author = inc.ReadString(); + bool hasAuthor = inc.ReadBoolean(); + + if (hasAuthor) + { + isOwn = inc.ReadByte() == 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); + } + break; + case ReadyCheckState.Update: + crewManager.ActiveReadyCheck.time = inc.ReadSingle(); + ReadyStatus newState = (ReadyStatus) inc.ReadByte(); + byte targetId = inc.ReadByte(); + 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() + { + 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/GameSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs index e3ddc5cf1..80e58d4ae 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs @@ -1335,7 +1335,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..27c833862 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) + { + spriteBatch.Draw(VineAtlas.Texture, pos + vine.offset, vineSprite.SourceRect, color, 0f, vineSprite.AbsoluteOrigin, scale, SpriteEffects.None, layer3); + } + + if (DecayAtlas != null) + { + 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/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/ItemContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs index 205a72010..218269d2b 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.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..9544a0116 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; } @@ -223,6 +223,7 @@ namespace Barotrauma.Items.Components FlowPercentage = msg.ReadRangedInteger(-10, 10) * 10.0f; IsActive = msg.ReadBoolean(); + Hijacked = msg.ReadBoolean(); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index b59955c76..113d693e5 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; @@ -28,11 +29,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; @@ -60,8 +68,6 @@ namespace Barotrauma.Items.Components private const float DisruptionUpdateInterval = 0.2f; private float disruptionUpdateTimer; - private float zoomSqrt; - private float showDirectionalIndicatorTimer; //Vector2 = vector from the ping source to the position of the disruption @@ -114,6 +120,10 @@ 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(); + partial void InitProjSpecific(XElement element) { System.Diagnostics.Debug.Assert(Enum.GetValues(typeof(BlipType)).Cast().All(t => blipColorGradient.ContainsKey(t))); @@ -214,11 +224,14 @@ namespace Barotrauma.Items.Components }; passiveTickBox.TextBlock.OverrideTextColor(GUI.Style.TextColor); activeTickBox.TextBlock.OverrideTextColor(GUI.Style.TextColor); + textBlocksToScaleAndNormalize.Add(passiveTickBox.TextBlock); + textBlocksToScaleAndNormalize.Add(activeTickBox.TextBlock); - 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); + 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) @@ -238,7 +251,7 @@ namespace Barotrauma.Items.Components new GUIFrame(new RectTransform(new Vector2(0.8f, 0.01f), paddedControlContainer.RectTransform, Anchor.Center), style: "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 +268,11 @@ 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); 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); @@ -275,7 +288,7 @@ namespace Barotrauma.Items.Components sonarView.RectTransform.ScaleBasis = ScaleBasis.Smallest; sonarView.RectTransform.SetPosition(Anchor.CenterRight); sonarView.RectTransform.Resize(GUISizeCalculation); - GUITextBlock.AutoScaleAndNormalize(passiveTickBox.TextBlock, activeTickBox.TextBlock, zoomText, directionalModeSwitchText); + GUITextBlock.AutoScaleAndNormalize(textBlocksToScaleAndNormalize); } } @@ -293,10 +306,40 @@ namespace Barotrauma.Items.Components { base.OnItemLoaded(); zoomSlider.BarScroll = MathUtils.InverseLerp(MinZoom, MaxZoom, zoom); + if (HasMineralScanner) { AddMineralScannerSwitchToGUI(); } //make the sonarView customcomponent render the steering view so it gets drawn in front of the sonar item.GetComponent()?.AttachToSonarHUD(sonarView); } + private void AddMineralScannerSwitchToGUI() + { + // First adjust other elements of the lower area + zoomSlider.Parent.RectTransform.RelativeSize = new Vector2(1.0f, 0.3f); + directionalModeSwitch.Parent.RectTransform.RelativeSize = new Vector2(1.0f, 0.3f); + directionalModeSwitch.Parent.RectTransform.SetPosition(Anchor.Center); + + // Then add the scanner switch + var mineralScannerFrame = new GUIFrame(new RectTransform(new Vector2(1, 0.3f), lowerAreaFrame.RectTransform, Anchor.BottomCenter), style: null); + 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); + GUITextBlock.AutoScaleAndNormalize(textBlocksToScaleAndNormalize); + } + public override void UpdateHUD(Character character, float deltaTime, Camera cam) { showDirectionalIndicatorTimer -= deltaTime; @@ -339,6 +382,32 @@ namespace Barotrauma.Items.Components Vector2.DistanceSquared(sonarView.Rect.Center.ToVector2(), PlayerInput.MousePosition) < (sonarView.Rect.Width / 2 * sonarView.Rect.Width / 2); + if (HasMineralScanner && Level.Loaded != null && !Level.Loaded.Generating) + { + if (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,12 +417,16 @@ namespace Barotrauma.Items.Components if (Level.Loaded != null) { + 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)) + LevelObjectManager objManager = Level.Loaded.LevelObjectManager; + float pingRange = range * activePing.State / zoom; + foreach (LevelObject levelObject in objManager.GetAllObjects(transducerCenter, pingRange)) { + 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) { @@ -363,6 +436,10 @@ namespace Barotrauma.Items.Components { levelTriggerFlows.Add(trigger, flow); } + if (!string.IsNullOrWhiteSpace(trigger.InfectIdentifier) && Vector2.DistanceSquared(transducerCenter, trigger.WorldPosition) < pingRange / 2 * pingRange / 2) + { + ballastFloraSpores.Add(trigger); + } } } } @@ -400,6 +477,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 +811,24 @@ namespace Barotrauma.Items.Components } } + if (HasMineralScanner && useMineralScanner && CurrentMode == Mode.Active && MineralClusters != null) + { + var maxMineralScanRangeSquared = Range * Range; + foreach (var t in MineralClusters) + { + var unobtainedMinerals = t.Item2.Where(i => i != null && i.GetRootInventoryOwner() == i); + if (unobtainedMinerals.None()) { continue; } + if (Vector2.DistanceSquared(transducerCenter, t.Item1) > maxMineralScanRangeSquared) { continue; } + var i = unobtainedMinerals.FirstOrDefault(); + if (i == null) { continue; } + DrawMarker(spriteBatch, + i.Name, null, i, + t.Item1, transducerCenter, + displayScale, center, DisplayRadius * 0.95f, + onlyShowTextOnMouseOver: true); + } + } + foreach (Submarine sub in Submarine.Loaded) { if (!sub.ShowSonarMarker) { continue; } @@ -1334,7 +1442,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 +1500,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; } } @@ -1446,6 +1566,8 @@ namespace Barotrauma.Items.Components sprite.Remove(); } targetIcons.Clear(); + + MineralClusters = null; } public void ClientWrite(IWriteMessage msg, object[] extraData = null) @@ -1460,6 +1582,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 +1594,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 +1603,7 @@ namespace Barotrauma.Items.Components { directionT = msg.ReadRangedSingle(0.0f, 1.0f, 8); } + mineralScanner = msg.ReadBoolean(); } if (correctionTimer > 0.0f) @@ -1500,6 +1625,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 +1640,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..fb5036c77 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs @@ -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; 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..6e57ae7c3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs @@ -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.00001f); 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..bcb79e660 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs @@ -288,72 +288,77 @@ namespace Barotrauma.Items.Components if (!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; GUI.DrawLine(spriteBatch, drawPos, drawPos + new Vector2((float)Math.Cos((maxRotation + minRotation) / 2), (float)Math.Sin((maxRotation + minRotation) / 2)) * widgetRadius, Color.LightGreen); + 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); + } + Widget minRotationWidget = GetWidget("minrotation", spriteBatch, size: 10, initMethod: (widget) => { - widget.Selected += () => + 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.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; + } + RotationLimits = RotationLimits; + 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: 10, initMethod: (widget) => { widget.Selected += () => @@ -383,6 +388,7 @@ namespace Barotrauma.Items.Components minRotation = maxRotation; maxRotation = temp; } + RotationLimits = RotationLimits; MapEntity.DisableSelect = true; }; widget.PreUpdate += (deltaTime) => @@ -398,7 +404,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); }; }); @@ -580,7 +586,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..dceab415f 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 @@ -239,6 +239,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.IsWiringMode() && !isWire && parentInventory == null; + bool renderTransparent = isWiringMode && GetComponent() == null; + if (renderTransparent) { color *= 0.15f; } BrokenItemSprite fadeInBrokenSprite = null; float fadeInBrokenSpriteAlpha = 0.0f; @@ -269,6 +273,7 @@ namespace Barotrauma } float depth = GetDrawDepth(); + if (isWiringMode && isLogic && !PlayerInput.IsShiftDown()) { depth = 0.01f; } if (activeSprite != null) { SpriteEffects oldEffects = activeSprite.effects; @@ -296,6 +301,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 +323,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 +381,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 +401,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 +433,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 +529,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 +1377,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 +1409,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 +1473,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 +1489,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..c1786c854 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Creatures/BallastFloraBehavior.cs @@ -0,0 +1,333 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using Barotrauma.Items.Components; +using Barotrauma.Networking; +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; } + + private float RandRotation() => Rand.Range(MinRotation, MaxRotation); + private float RandVelocity() => Rand.Range(MinVelocity, MaxVelocity); + + public void Emit(Vector2 pos) + { + GameMain.ParticleManager.CreateParticle(Identifier, pos, RandRotation(), RandVelocity()); + } + + 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(); + + 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 "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 static readonly Color DarkColor = new Color(25, 25, 25); + + public void Draw(SpriteBatch spriteBatch) + { + const float zStep = 0.00001f; + 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) + { + spriteBatch.Draw(branchAtlas.Texture, pos + branch.offset, branchSprite.SourceRect, branchColor, 0f, branchSprite.AbsoluteOrigin, BaseBranchScale * branch.VineStep, SpriteEffects.None, layer2); + } + + if (decayAtlas != null && isDamaged) + { + 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 (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; + } + } + } + + + public void ClientRead(IReadMessage msg, NetworkHeader header) + { + switch (header) + { + case NetworkHeader.Infect: + ushort itemId = msg.ReadUInt16(); + bool infect = msg.ReadBoolean(); + if (Entity.FindEntityByID(itemId) is Item item) + { + if (infect) + { + ClaimTarget(item, null); + } + else + { + RemoveClaim(itemId); + } + } + break; + case NetworkHeader.BranchCreate: + int parentId = msg.ReadInt32(); + BallastFloraBranch branch = ReadBranch(msg); + + UpdateConnections(branch, Branches.FirstOrDefault(b => b.ID == parentId)); + 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); } + 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; + } + 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, + 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..3663d2bb5 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) @@ -607,6 +611,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..923c7a848 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,73 @@ 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); + 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); } - } - - foreach (VoronoiCell cell in cells) - { 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 +88,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 +97,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,7 +123,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; } rightNormal = Vector2.UnitX; @@ -127,14 +133,13 @@ namespace Barotrauma float point1UV = MathUtils.WrapAngleTwoPi(MathUtils.VectorToAngle(edge.Point1 - cell.Center)); float point2UV = MathUtils.WrapAngleTwoPi(MathUtils.VectorToAngle(edge.Point2 - cell.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 +152,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 +163,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..cfa02438c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs @@ -1,14 +1,12 @@ using Barotrauma.Lights; +using Barotrauma.Networking; using Barotrauma.Particles; using Barotrauma.Sounds; -using Barotrauma.Networking; +using Barotrauma.SpriteDeformations; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Xml.Linq; -using Barotrauma.SpriteDeformations; -using System.Linq; -using FarseerPhysics.Dynamics; namespace Barotrauma { @@ -77,7 +75,6 @@ namespace Barotrauma partial void InitProjSpecific() { Sprite?.EnsureLazyLoaded(); - SpecularSprite?.EnsureLazyLoaded(); Prefab.DeformableSprite?.EnsureLazyLoaded(); CurrentSwingAmount = Prefab.SwingAmountRad; @@ -98,7 +95,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]; @@ -232,17 +229,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 +259,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 +270,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..bd027c920 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)); + + wallEdgeEffect.Texture = level.GenerationParams.DestructibleWallEdgeSprite?.Texture ?? level.GenerationParams.WallEdgeSprite.Texture; + wallEdgeEffect.World = wall.GetTransform() * transformMatrix; + wallEdgeEffect.Alpha = wall.Alpha; + wallEdgeEffect.CurrentTechnique.Passes[0].Apply(); + graphicsDevice.SetVertexBuffer(wall.WallEdgeBuffer); + graphicsDevice.DrawPrimitives(PrimitiveType.TriangleList, 0, (int)Math.Floor(wall.WallEdgeBuffer.VertexCount / 3.0f)); + + if (destructibleWall.Damage <= 0.0f) { continue; } + wallEdgeEffect.Texture = level.GenerationParams.WallSpriteDestroyed.Texture; + wallEdgeEffect.Alpha = MathHelper.Lerp(0.2f, 1.0f, destructibleWall.Damage / destructibleWall.MaxHealth) * wall.Alpha; + wallEdgeEffect.World = wall.GetTransform() * transformMatrix; + wallEdgeEffect.CurrentTechnique.Passes[0].Apply(); + graphicsDevice.SetVertexBuffer(wall.WallEdgeBuffer); + graphicsDevice.DrawPrimitives(PrimitiveType.TriangleList, 0, (int)Math.Floor(wall.WallEdgeBuffer.VertexCount / 3.0f)); + } } - 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..639b68d1b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs @@ -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 lights) + { + if (!light.Enabled) { continue; } + light.Update(deltaTime); + } + } + + public void RenderLightMap(GraphicsDevice graphics, SpriteBatch spriteBatch, Camera cam, RenderTarget2D backgroundObstructor = null) { if (!LightingEnabled) { return; } @@ -174,7 +183,7 @@ namespace Barotrauma.Lights foreach (LightSource light in lights) { if (!light.Enabled) { continue; } - if ((light.Color.A < 1 || light.Range < 1.0f) && !light.LightSourceParams.OverrideLightSpriteAlpha.HasValue) { continue; } + if ((light.Color.A < 1 || light.Range < 1.0f || light.CurrentBrightness <= 0.0f) && !light.LightSourceParams.OverrideLightSpriteAlpha.HasValue) { continue; } if (light.ParentBody != null) { light.Position = light.ParentBody.DrawPosition; @@ -205,7 +214,7 @@ namespace Barotrauma.Lights { if (light.IsBackground) { 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,6 +224,7 @@ 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; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs index 438cbb3cb..42f1bf020 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,41 @@ namespace Barotrauma.Lights texture = LightTexture; diffToSub = new Dictionary(); if (addLight) { GameMain.LightManager.AddLight(this); } + } + public void Update(float deltaTime) + { + if (lightSourceParams.BlinkFrequency > 0.0f) + { + blinkTimer = (blinkTimer + deltaTime * lightSourceParams.BlinkFrequency) % 1.0f; + } + + if (lightSourceParams.PulseFrequency > 0.0f) + { + pulseState = (pulseState + deltaTime * lightSourceParams.PulseFrequency) % 1.0f; + } + + if (blinkTimer > 0.5f) + { + CurrentBrightness = 0.0f; + } + else + { + float flicker = 0.0f; + float pulse = 0.0f; + if (lightSourceParams.Flicker > 0.0f) + { + flickerState += deltaTime * lightSourceParams.FlickerSpeed; + flickerState %= 255; + flicker = PerlinNoise.GetPerlin(flickerState, flickerState * 0.5f) * lightSourceParams.Flicker; + } + if (lightSourceParams.PulseFrequency > 0.0f && lightSourceParams.PulseAmount > 0.0f) + { + //oscillate between 0-1 + pulse = (float)(Math.Sin(pulseState * MathHelper.TwoPi) + 1.0f) / 2.0f * lightSourceParams.PulseAmount; + } + CurrentBrightness = (1.0f - flicker) * (1.0f - pulse); + } } /// @@ -508,13 +606,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 +981,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 +1123,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 +1200,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 +1217,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 +1242,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 +1287,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 +1309,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 +1331,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..188761e82 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,40 @@ 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; + if (connection.LevelData.InitialDepth * Physics.DisplayToRealWorldRatio > connection.LevelData.RealWorldCrushDepth) + { + crushDepthWarningIconStyle = GUI.Style.GetComponentStyle("CrushDepthWarningHighIcon"); + tooltip = "crushdepthwarninghigh"; + } + else if ((connection.LevelData.InitialDepth + connection.LevelData.Size.Y) * Physics.DisplayToRealWorldRatio > connection.LevelData.RealWorldCrushDepth) + { + 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)(Submarine.MainSub?.RealWorldCrushDepth ?? Level.DefaultRealWorldCrushDepth)).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..b9748ef25 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs @@ -329,8 +329,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 +897,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 +913,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 +928,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..f8a64e70a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -238,6 +238,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,6 +254,10 @@ namespace Barotrauma thickness: Math.Max(1, (int)(2 / Screen.Selected.Cam.Zoom))); } + bool isWiringMode = editing && SubEditorScreen.IsWiringMode(); + + if (isWiringMode) { color *= 0.15f; } + Vector2 drawOffset = Submarine == null ? Vector2.Zero : Submarine.DrawPosition; float depth = GetDrawDepth(); @@ -261,7 +266,7 @@ namespace Barotrauma 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) { 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..47fafd5f4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -903,6 +903,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 +979,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 + ")."; 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/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/Particles/Decal.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/Decal.cs deleted file mode 100644 index 41008a008..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/Decal.cs +++ /dev/null @@ -1,131 +0,0 @@ -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; -using System; - -namespace Barotrauma.Particles -{ - class Decal - { - public readonly DecalPrefab Prefab; - private Vector2 position; - - public readonly Sprite Sprite; - - private float fadeTimer; - - public float FadeTimer - { - get { return fadeTimer; } - set { fadeTimer = MathHelper.Clamp(value, 0.0f, LifeTime); } - } - - public float FadeInTime - { - get { return Prefab.FadeInTime; } - } - - public float FadeOutTime - { - get { return Prefab.FadeOutTime; } - } - - public float LifeTime - { - get { return Prefab.LifeTime; } - } - - public Color Color - { - get; - set; - } - - public Vector2 WorldPosition - { - get - { - Vector2 worldPos = position - + clippedSourceRect.Size.ToVector2() / 2 * scale - + hull.Rect.Location.ToVector2(); - if (hull.Submarine != null) { worldPos += hull.Submarine.DrawPosition; } - return worldPos; - } - } - - private Hull hull; - - private float scale; - - private Rectangle clippedSourceRect; - - public Decal(DecalPrefab prefab, float scale, Vector2 worldPosition, Hull hull) - { - Prefab = prefab; - - this.hull = hull; - - //transform to hull-relative coordinates so we don't have to worry about the hull moving - position = worldPosition - hull.WorldRect.Location.ToVector2(); - - Vector2 drawPos = position + hull.Rect.Location.ToVector2(); - - Sprite = prefab.Sprites[Rand.Range(0, prefab.Sprites.Count, Rand.RandSync.Unsynced)]; - Color = prefab.Color; - - Rectangle drawRect = new Rectangle( - (int)(drawPos.X - Sprite.size.X / 2 * scale), - (int)(drawPos.Y + Sprite.size.Y / 2 * scale), - (int)(Sprite.size.X * scale), - (int)(Sprite.size.Y * scale)); - - Rectangle overFlowAmount = new Rectangle( - (int)Math.Max(hull.Rect.X - drawRect.X, 0.0f), - (int)Math.Max(drawRect.Y - hull.Rect.Y, 0.0f), - (int)Math.Max(drawRect.Right - hull.Rect.Right, 0.0f), - (int)Math.Max((hull.Rect.Y - hull.Rect.Height) - (drawRect.Y - drawRect.Height), 0.0f)); - - clippedSourceRect = new Rectangle( - Sprite.SourceRect.X + (int)(overFlowAmount.X / scale), - Sprite.SourceRect.Y + (int)(overFlowAmount.Y / scale), - Sprite.SourceRect.Width - (int)((overFlowAmount.X + overFlowAmount.Width) / scale), - Sprite.SourceRect.Height - (int)((overFlowAmount.Y + overFlowAmount.Height) / scale)); - - position -= new Vector2(Sprite.size.X / 2 * scale - overFlowAmount.X, -Sprite.size.Y / 2 * scale + overFlowAmount.Y); - - this.scale = scale; - } - - public void Update(float deltaTime) - { - fadeTimer += deltaTime; - } - - public void StopFadeIn() - { - Color *= GetAlpha(); - fadeTimer = Prefab.FadeInTime; - } - - public void Draw(SpriteBatch spriteBatch, Hull hull, float depth) - { - Vector2 drawPos = position + hull.Rect.Location.ToVector2(); - if (hull.Submarine != null) { drawPos += hull.Submarine.DrawPosition; } - drawPos.Y = -drawPos.Y; - - spriteBatch.Draw(Sprite.Texture, drawPos, clippedSourceRect, Color * GetAlpha(), 0, Vector2.Zero, scale, SpriteEffects.None, depth); - } - - private float GetAlpha() - { - if (fadeTimer < Prefab.FadeInTime) - { - return fadeTimer / Prefab.FadeInTime; - } - else if (fadeTimer > Prefab.LifeTime - Prefab.FadeOutTime) - { - return (Prefab.LifeTime - fadeTimer) / Prefab.FadeOutTime; - } - return 1.0f; - } - } -} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/DecalManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/DecalManager.cs deleted file mode 100644 index 6548582bf..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/DecalManager.cs +++ /dev/null @@ -1,75 +0,0 @@ -using Microsoft.Xna.Framework; -using System.Collections.Generic; -using System.Xml.Linq; -using System.Linq; - -namespace Barotrauma.Particles -{ - class DecalManager - { - public PrefabCollection Prefabs { get; private set; } - - public DecalManager() - { - Prefabs = new PrefabCollection(); - foreach (ContentFile configFile in GameMain.Instance.GetFilesOfType(ContentType.Decals)) - { - LoadFromFile(configFile); - } - } - - public void LoadFromFile(ContentFile configFile) - { - XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); - if (doc == null) { return; } - - bool allowOverriding = false; - var mainElement = doc.Root; - if (doc.Root.IsOverride()) - { - mainElement = doc.Root.FirstElement(); - allowOverriding = true; - } - - foreach (XElement sourceElement in mainElement.Elements()) - { - var element = sourceElement.IsOverride() ? sourceElement.FirstElement() : sourceElement; - string name = element.Name.ToString().ToLowerInvariant(); - if (Prefabs.ContainsKey(name)) - { - if (allowOverriding || sourceElement.IsOverride()) - { - DebugConsole.NewMessage($"Overriding the existing decal prefab '{name}' using the file '{configFile.Path}'", Color.Yellow); - } - else - { - DebugConsole.ThrowError($"Error in '{configFile.Path}': Duplicate decal prefab '{name}' found in '{configFile.Path}'! Each decal prefab must have a unique name. " + - "Use tags to override prefabs."); - continue; - } - - } - - Prefabs.Add(new DecalPrefab(element, configFile), allowOverriding || sourceElement.IsOverride()); - } - } - - public void RemoveByFile(string filePath) - { - Prefabs.RemoveByFile(filePath); - } - - public Decal CreateDecal(string decalName, float scale, Vector2 worldPosition, Hull hull) - { - if (!Prefabs.ContainsKey(decalName.ToLowerInvariant())) - { - DebugConsole.ThrowError("Decal prefab " + decalName + " not found!"); - return null; - } - - DecalPrefab prefab = Prefabs[decalName]; - - return new Decal(prefab, scale, worldPosition, hull); - } - } -} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/DecalPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/DecalPrefab.cs deleted file mode 100644 index da75fd497..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/DecalPrefab.cs +++ /dev/null @@ -1,73 +0,0 @@ -using Microsoft.Xna.Framework; -using System; -using System.Collections.Generic; -using System.Xml.Linq; - -namespace Barotrauma.Particles -{ - class DecalPrefab : IPrefab, IDisposable - { - public readonly string Name; - - public string OriginalName { get { return Name; } } - - private string _identifier; - public string Identifier - { - get - { - if (_identifier == null) - { - _identifier = Name.ToLowerInvariant(); - } - return _identifier; - } - } - - public string FilePath { get; private set; } - - public ContentPackage ContentPackage { get; private set; } - - public void Dispose() - { - foreach (Sprite spr in Sprites) - { - spr.Remove(); - } - Sprites.Clear(); - } - - public readonly List Sprites; - - public readonly Color Color; - - public readonly float LifeTime; - public readonly float FadeOutTime; - public readonly float FadeInTime; - - public DecalPrefab(XElement element, ContentFile file) - { - Name = element.Name.ToString(); - - FilePath = file.Path; - - ContentPackage = file.ContentPackage; - - Sprites = new List(); - - foreach (XElement subElement in element.Elements()) - { - if (subElement.Name.ToString().Equals("sprite", StringComparison.OrdinalIgnoreCase)) - { - Sprites.Add(new Sprite(subElement)); - } - } - - Color = new Color(element.GetAttributeVector4("color", Vector4.One)); - - LifeTime = element.GetAttributeFloat("lifetime", 10.0f); - FadeOutTime = Math.Min(LifeTime, element.GetAttributeFloat("fadeouttime", 1.0f)); - FadeInTime = Math.Min(LifeTime - FadeOutTime, element.GetAttributeFloat("fadeintime", 0.0f)); - } - } -} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs index 8030da641..c33caf404 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs @@ -483,7 +483,7 @@ namespace Barotrauma.Particles if (prefab.GrowTime > 0.0f && totalLifeTime - lifeTime < prefab.GrowTime) { - drawSize *= ((totalLifeTime - lifeTime) / prefab.GrowTime); + drawSize *= MathUtils.SmoothStep((totalLifeTime - lifeTime) / prefab.GrowTime); } Color currColor = new Color(color.ToVector4() * ColorMultiplier); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index a006f00f9..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); @@ -2786,9 +2789,7 @@ namespace Barotrauma.CharacterEditor }; // Spacing new GUIFrame(new RectTransform(buttonSize / 2, layoutGroup.RectTransform), style: null) { CanBeFocused = false }; - Vector2 messageBoxRelSize = new Vector2(0.5f, 0.5f); - int messageBoxWidth = GameMain.GraphicsWidth / 2; - int messageBoxHeight = GameMain.GraphicsHeight / 2; + Vector2 messageBoxRelSize = new Vector2(0.5f, 0.7f); var saveRagdollButton = new GUIButton(new RectTransform(buttonSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("SaveRagdoll")); saveRagdollButton.OnClicked += (button, userData) => { @@ -2908,12 +2909,12 @@ namespace Barotrauma.CharacterEditor { var box = new GUIMessageBox(GetCharacterEditorTranslation("SaveAnimation"), string.Empty, new string[] { TextManager.Get("Cancel"), TextManager.Get("Save") }, messageBoxRelSize); var textArea = new GUIFrame(new RectTransform(new Vector2(1, 0.1f), box.Content.RectTransform) { MinSize = new Point(350, 30) }, style: null); - var inputLabel = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), textArea.RectTransform) { MinSize = new Point(250, 30) }, $"{GetCharacterEditorTranslation("ProvideFileName")}: "); - var inputField = new GUITextBox(new RectTransform(new Vector2(0.5f, 1), textArea.RectTransform, Anchor.TopRight) { MinSize = new Point(100, 30) }, CurrentAnimation.Name); + var inputLabel = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), textArea.RectTransform, Anchor.CenterLeft) { MinSize = new Point(250, 30) }, $"{GetCharacterEditorTranslation("ProvideFileName")}: "); + var inputField = new GUITextBox(new RectTransform(new Vector2(0.45f, 1), textArea.RectTransform, Anchor.CenterRight) { MinSize = new Point(100, 30) }, CurrentAnimation.Name); // Type filtering var typeSelectionArea = new GUIFrame(new RectTransform(new Vector2(1f, 0.1f), box.Content.RectTransform) { MinSize = new Point(0, 30) }, style: null); - var typeLabel = new GUITextBlock(new RectTransform(new Vector2(0.4f, 1), typeSelectionArea.RectTransform, Anchor.TopCenter, Pivot.TopRight), $"{GetCharacterEditorTranslation("SelectAnimationType")}: "); - var typeDropdown = new GUIDropDown(new RectTransform(new Vector2(0.4f, 1), typeSelectionArea.RectTransform, Anchor.TopCenter, Pivot.TopLeft), elementCount: 4); + var typeLabel = new GUITextBlock(new RectTransform(new Vector2(0.45f, 1), typeSelectionArea.RectTransform, Anchor.CenterLeft), $"{GetCharacterEditorTranslation("SelectAnimationType")}: "); + var typeDropdown = new GUIDropDown(new RectTransform(new Vector2(0.45f, 1), typeSelectionArea.RectTransform, Anchor.CenterRight), elementCount: 4); foreach (object enumValue in Enum.GetValues(typeof(AnimationType))) { if (!(enumValue is AnimationType.NotDefined)) @@ -2964,8 +2965,8 @@ namespace Barotrauma.CharacterEditor deleteButton.Enabled = false; // Type filtering var typeSelectionArea = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.1f), loadBox.Content.RectTransform) { MinSize = new Point(0, 30) }, style: null); - var typeLabel = new GUITextBlock(new RectTransform(new Vector2(0.4f, 1), typeSelectionArea.RectTransform, Anchor.TopCenter, Pivot.TopRight), $"{GetCharacterEditorTranslation("SelectAnimationType")}: "); - var typeDropdown = new GUIDropDown(new RectTransform(new Vector2(0.4f, 1), typeSelectionArea.RectTransform, Anchor.TopCenter, Pivot.TopLeft), elementCount: 4); + var typeLabel = new GUITextBlock(new RectTransform(new Vector2(0.45f, 1), typeSelectionArea.RectTransform, Anchor.CenterLeft), $"{GetCharacterEditorTranslation("SelectAnimationType")}: "); + var typeDropdown = new GUIDropDown(new RectTransform(new Vector2(0.45f, 1), typeSelectionArea.RectTransform, Anchor.CenterRight), elementCount: 4); foreach (object enumValue in Enum.GetValues(typeof(AnimationType))) { if (!(enumValue is AnimationType.NotDefined)) 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..14aa68a2b 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,15 +910,17 @@ 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), @@ -934,7 +936,6 @@ namespace Barotrauma } }; frame.RectTransform.MinSize = missionTypeTickBoxes[index].RectTransform.MinSize; - index++; } clientDisabledElements.AddRange(missionTypeTickBoxes); @@ -1485,7 +1486,7 @@ namespace Barotrauma return true; } }; - } + } } private void CreateChangesPendingText() @@ -2509,7 +2510,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 +2525,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 +2540,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 +2555,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 +2977,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 +3002,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 +3080,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..0f4c8c0b8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs @@ -1061,7 +1061,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/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index c5eb94a9d..62a8c6b3c 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,23 @@ 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(); + + 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(); @@ -630,35 +647,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 +752,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,8 +1015,7 @@ 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) @@ -1150,7 +1170,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 @@ -1853,6 +1873,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 +2689,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); @@ -3710,6 +3734,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 +3981,11 @@ namespace Barotrauma CloseItem(); } + if (lightingEnabled) + { + GameMain.LightManager?.Update((float)deltaTime); + } + if (contextMenu != null) { Rectangle expandedRect = contextMenu.Rect; @@ -4034,7 +4081,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 +4137,7 @@ namespace Barotrauma if (!newItem.Removed) { - BulkItemBufferInUse = true; + BulkItemBufferInUse = ItemAddMutex; BulkItemBuffer.Add(new AddOrDeleteCommand(new List { newItem }, false)); } @@ -4162,7 +4209,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 +4221,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 +4364,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 +4531,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/SoundManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs index 255a8e072..03420baa7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs @@ -730,6 +730,7 @@ namespace Barotrauma.Sounds else { playingChannels[i][j].Dispose(); + playingChannels[i][j] = null; } } else if (playingChannels[i][j].FadingOutAndDisposing) @@ -738,6 +739,7 @@ namespace Barotrauma.Sounds if (playingChannels[i][j].Gain <= 0.0f) { playingChannels[i][j].Dispose(); + playingChannels[i][j] = null; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs index 495e35324..02b85c59f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs @@ -499,6 +499,12 @@ namespace Barotrauma private static void UpdateWaterFlowSounds(float deltaTime) { if (FlowSounds.Count == 0) { return; } + + for (int i = 0; i < targetFlowLeft.Length; i++) + { + targetFlowLeft[i] = 0.0f; + targetFlowRight[i] = 0.0f; + } Vector2 listenerPos = new Vector2(GameMain.SoundManager.ListenerPosition.X, GameMain.SoundManager.ListenerPosition.Y); foreach (Gap gap in Gap.GapList) @@ -619,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]); @@ -954,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"; } 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/HttpUtility.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/HttpUtility.cs deleted file mode 100644 index 9f0ee6910..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/HttpUtility.cs +++ /dev/null @@ -1,773 +0,0 @@ -// -// System.Web.HttpUtility -// -// Authors: -// Patrik Torstensson (Patrik.Torstensson@labs2.com) -// Wictor Wilén (decode/encode functions) (wictor@ibizkit.se) -// Tim Coleman (tim@timcoleman.com) -// Gonzalo Paniagua Javier (gonzalo@ximian.com) -// -// Copyright (C) 2005-2010 Novell, Inc (http://www.novell.com) -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Globalization; -using Barotrauma.IO; -using System.Security.Permissions; -using System.Text; - -namespace RestSharp.Contrib -{ - - //#if !MONOTOUCH - // // CAS - no InheritanceDemand here as the class is sealed - // [AspNetHostingPermission(SecurityAction.LinkDemand, Level = AspNetHostingPermissionLevel.Minimal)] - //#endif - public sealed class HttpUtility - { - sealed class HttpQSCollection : NameValueCollection - { - public override string ToString() - { - int count = Count; - if (count == 0) - return ""; - StringBuilder sb = new StringBuilder(); - string[] keys = AllKeys; - for (int i = 0; i < count; i++) - { - sb.AppendFormat("{0}={1}&", keys[i], this[keys[i]]); - } - if (sb.Length > 0) - sb.Length--; - return sb.ToString(); - } - } - - #region Constructors - - public HttpUtility() - { - } - - #endregion // Constructors - - #region Methods - - /* - public static void HtmlAttributeEncode(string s, TextWriter output) - { - if (output == null) - { -#if NET_4_0 - throw new ArgumentNullException ("output"); -#else - throw new NullReferenceException(".NET emulation"); -#endif - } -#if NET_4_0 - HttpEncoder.Current.HtmlAttributeEncode (s, output); -#else - output.Write(HttpEncoder.HtmlAttributeEncode(s)); -#endif - } - */ - - public static string HtmlAttributeEncode(string s) - { -#if NET_4_0 - if (s == null) - return null; - - using (var sw = new StringWriter ()) { - HttpEncoder.Current.HtmlAttributeEncode (s, sw); - return sw.ToString (); - } -#else - return HttpEncoder.HtmlAttributeEncode(s); -#endif - } - - public static string UrlDecode(string str) - { - return UrlDecode(str, Encoding.UTF8); - } - - static char[] GetChars(System.IO.MemoryStream b, Encoding e) - { - return e.GetChars(b.GetBuffer(), 0, (int)b.Length); - } - - static void WriteCharBytes(IList buf, char ch, Encoding e) - { - if (ch > 255) - { - foreach (byte b in e.GetBytes(new char[] { ch })) - buf.Add(b); - } - else - buf.Add((byte)ch); - } - - public static string UrlDecode(string s, Encoding e) - { - if (null == s) - return null; - - if (s.IndexOf('%') == -1 && s.IndexOf('+') == -1) - return s; - - if (e == null) - e = Encoding.UTF8; - - long len = s.Length; - var bytes = new List(); - int xchar; - char ch; - - for (int i = 0; i < len; i++) - { - ch = s[i]; - if (ch == '%' && i + 2 < len && s[i + 1] != '%') - { - if (s[i + 1] == 'u' && i + 5 < len) - { - // unicode hex sequence - xchar = GetChar(s, i + 2, 4); - if (xchar != -1) - { - WriteCharBytes(bytes, (char)xchar, e); - i += 5; - } - else - WriteCharBytes(bytes, '%', e); - } - else if ((xchar = GetChar(s, i + 1, 2)) != -1) - { - WriteCharBytes(bytes, (char)xchar, e); - i += 2; - } - else - { - WriteCharBytes(bytes, '%', e); - } - continue; - } - - if (ch == '+') - WriteCharBytes(bytes, ' ', e); - else - WriteCharBytes(bytes, ch, e); - } - - byte[] buf = bytes.ToArray(); - bytes = null; - return e.GetString(buf); - - } - - public static string UrlDecode(byte[] bytes, Encoding e) - { - if (bytes == null) - return null; - - return UrlDecode(bytes, 0, bytes.Length, e); - } - - static int GetInt(byte b) - { - char c = (char)b; - if (c >= '0' && c <= '9') - return c - '0'; - - if (c >= 'a' && c <= 'f') - return c - 'a' + 10; - - if (c >= 'A' && c <= 'F') - return c - 'A' + 10; - - return -1; - } - - static int GetChar(byte[] bytes, int offset, int length) - { - int value = 0; - int end = length + offset; - for (int i = offset; i < end; i++) - { - int current = GetInt(bytes[i]); - if (current == -1) - return -1; - value = (value << 4) + current; - } - - return value; - } - - static int GetChar(string str, int offset, int length) - { - int val = 0; - int end = length + offset; - for (int i = offset; i < end; i++) - { - char c = str[i]; - if (c > 127) - return -1; - - int current = GetInt((byte)c); - if (current == -1) - return -1; - val = (val << 4) + current; - } - - return val; - } - - public static string UrlDecode(byte[] bytes, int offset, int count, Encoding e) - { - if (bytes == null) - return null; - if (count == 0) - return String.Empty; - - if (bytes == null) - throw new ArgumentNullException("bytes"); - - if (offset < 0 || offset > bytes.Length) - throw new ArgumentOutOfRangeException("offset"); - - if (count < 0 || offset + count > bytes.Length) - throw new ArgumentOutOfRangeException("count"); - - StringBuilder output = new StringBuilder(); - System.IO.MemoryStream acc = new System.IO.MemoryStream(); - - int end = count + offset; - int xchar; - for (int i = offset; i < end; i++) - { - if (bytes[i] == '%' && i + 2 < count && bytes[i + 1] != '%') - { - if (bytes[i + 1] == (byte)'u' && i + 5 < end) - { - if (acc.Length > 0) - { - output.Append(GetChars(acc, e)); - acc.SetLength(0); - } - xchar = GetChar(bytes, i + 2, 4); - if (xchar != -1) - { - output.Append((char)xchar); - i += 5; - continue; - } - } - else if ((xchar = GetChar(bytes, i + 1, 2)) != -1) - { - acc.WriteByte((byte)xchar); - i += 2; - continue; - } - } - - if (acc.Length > 0) - { - output.Append(GetChars(acc, e)); - acc.SetLength(0); - } - - if (bytes[i] == '+') - { - output.Append(' '); - } - else - { - output.Append((char)bytes[i]); - } - } - - if (acc.Length > 0) - { - output.Append(GetChars(acc, e)); - } - - acc = null; - return output.ToString(); - } - - public static byte[] UrlDecodeToBytes(byte[] bytes) - { - if (bytes == null) - return null; - - return UrlDecodeToBytes(bytes, 0, bytes.Length); - } - - public static byte[] UrlDecodeToBytes(string str) - { - return UrlDecodeToBytes(str, Encoding.UTF8); - } - - public static byte[] UrlDecodeToBytes(string str, Encoding e) - { - if (str == null) - return null; - - if (e == null) - throw new ArgumentNullException("e"); - - return UrlDecodeToBytes(e.GetBytes(str)); - } - - public static byte[] UrlDecodeToBytes(byte[] bytes, int offset, int count) - { - if (bytes == null) - return null; - if (count == 0) - return new byte[0]; - - int len = bytes.Length; - if (offset < 0 || offset >= len) - throw new ArgumentOutOfRangeException("offset"); - - if (count < 0 || offset > len - count) - throw new ArgumentOutOfRangeException("count"); - - System.IO.MemoryStream result = new System.IO.MemoryStream(); - int end = offset + count; - for (int i = offset; i < end; i++) - { - char c = (char)bytes[i]; - if (c == '+') - { - c = ' '; - } - else if (c == '%' && i < end - 2) - { - int xchar = GetChar(bytes, i + 1, 2); - if (xchar != -1) - { - c = (char)xchar; - i += 2; - } - } - result.WriteByte((byte)c); - } - - return result.ToArray(); - } - - public static string UrlEncode(string str) - { - return UrlEncode(str, Encoding.UTF8); - } - - public static string UrlEncode(string s, Encoding Enc) - { - if (s == null) - return null; - - if (s == String.Empty) - return String.Empty; - - bool needEncode = false; - int len = s.Length; - for (int i = 0; i < len; i++) - { - char c = s[i]; - if ((c < '0') || (c < 'A' && c > '9') || (c > 'Z' && c < 'a') || (c > 'z')) - { - if (HttpEncoder.NotEncoded(c)) - continue; - - needEncode = true; - break; - } - } - - if (!needEncode) - return s; - - // avoided GetByteCount call - byte[] bytes = new byte[Enc.GetMaxByteCount(s.Length)]; - int realLen = Enc.GetBytes(s, 0, s.Length, bytes, 0); - return Encoding.ASCII.GetString(UrlEncodeToBytes(bytes, 0, realLen)); - } - - public static string UrlEncode(byte[] bytes) - { - if (bytes == null) - return null; - - if (bytes.Length == 0) - return String.Empty; - - return Encoding.ASCII.GetString(UrlEncodeToBytes(bytes, 0, bytes.Length)); - } - - public static string UrlEncode(byte[] bytes, int offset, int count) - { - if (bytes == null) - return null; - - if (bytes.Length == 0) - return String.Empty; - - return Encoding.ASCII.GetString(UrlEncodeToBytes(bytes, offset, count)); - } - - public static byte[] UrlEncodeToBytes(string str) - { - return UrlEncodeToBytes(str, Encoding.UTF8); - } - - public static byte[] UrlEncodeToBytes(string str, Encoding e) - { - if (str == null) - return null; - - if (str.Length == 0) - return new byte[0]; - - byte[] bytes = e.GetBytes(str); - return UrlEncodeToBytes(bytes, 0, bytes.Length); - } - - public static byte[] UrlEncodeToBytes(byte[] bytes) - { - if (bytes == null) - return null; - - if (bytes.Length == 0) - return new byte[0]; - - return UrlEncodeToBytes(bytes, 0, bytes.Length); - } - - public static byte[] UrlEncodeToBytes(byte[] bytes, int offset, int count) - { - if (bytes == null) - return null; -#if NET_4_0 - return HttpEncoder.Current.UrlEncode (bytes, offset, count); -#else - return HttpEncoder.UrlEncodeToBytes(bytes, offset, count); -#endif - } - - public static string UrlEncodeUnicode(string str) - { - if (str == null) - return null; - - return Encoding.ASCII.GetString(UrlEncodeUnicodeToBytes(str)); - } - - public static byte[] UrlEncodeUnicodeToBytes(string str) - { - if (str == null) - return null; - - if (str.Length == 0) - return new byte[0]; - - System.IO.MemoryStream result = new System.IO.MemoryStream(str.Length); - foreach (char c in str) - { - HttpEncoder.UrlEncodeChar(c, result, true); - } - return result.ToArray(); - } - - /// - /// Decodes an HTML-encoded string and returns the decoded string. - /// - /// The HTML string to decode. - /// The decoded text. - public static string HtmlDecode(string s) - { -#if NET_4_0 - if (s == null) - return null; - - using (var sw = new StringWriter ()) { - HttpEncoder.Current.HtmlDecode (s, sw); - return sw.ToString (); - } -#else - return HttpEncoder.HtmlDecode(s); -#endif - } - - /// - /// Decodes an HTML-encoded string and sends the resulting output to a TextWriter output stream. - /// - /// The HTML string to decode - /// The TextWriter output stream containing the decoded string. - /* - public static void HtmlDecode(string s, TextWriter output) - { - if (output == null) - { -#if NET_4_0 - throw new ArgumentNullException ("output"); -#else - throw new NullReferenceException(".NET emulation"); -#endif - } - - if (!String.IsNullOrEmpty(s)) - { -#if NET_4_0 - HttpEncoder.Current.HtmlDecode (s, output); -#else - output.Write(HttpEncoder.HtmlDecode(s)); -#endif - } - } - */ - - public static string HtmlEncode(string s) - { -#if NET_4_0 - if (s == null) - return null; - - using (var sw = new StringWriter ()) { - HttpEncoder.Current.HtmlEncode (s, sw); - return sw.ToString (); - } -#else - return HttpEncoder.HtmlEncode(s); -#endif - } - - /// - /// HTML-encodes a string and sends the resulting output to a TextWriter output stream. - /// - /// The string to encode. - /// The TextWriter output stream containing the encoded string. - /* - public static void HtmlEncode(string s, TextWriter output) - { - if (output == null) - { -#if NET_4_0 - throw new ArgumentNullException ("output"); -#else - throw new NullReferenceException(".NET emulation"); -#endif - } - - if (!String.IsNullOrEmpty(s)) - { -#if NET_4_0 - HttpEncoder.Current.HtmlEncode (s, output); -#else - output.Write(HttpEncoder.HtmlEncode(s)); -#endif - } - } - */ - -#if NET_4_0 - public static string HtmlEncode (object value) - { - if (value == null) - return null; - - IHtmlString htmlString = value as IHtmlString; - if (htmlString != null) - return htmlString.ToHtmlString (); - - return HtmlEncode (value.ToString ()); - } - - public static string JavaScriptStringEncode (string value) - { - return JavaScriptStringEncode (value, false); - } - - public static string JavaScriptStringEncode (string value, bool addDoubleQuotes) - { - if (String.IsNullOrEmpty (value)) - return addDoubleQuotes ? "\"\"" : String.Empty; - - int len = value.Length; - bool needEncode = false; - char c; - for (int i = 0; i < len; i++) { - c = value [i]; - - if (c >= 0 && c <= 31 || c == 34 || c == 39 || c == 60 || c == 62 || c == 92) { - needEncode = true; - break; - } - } - - if (!needEncode) - return addDoubleQuotes ? "\"" + value + "\"" : value; - - var sb = new StringBuilder (); - if (addDoubleQuotes) - sb.Append ('"'); - - for (int i = 0; i < len; i++) { - c = value [i]; - if (c >= 0 && c <= 7 || c == 11 || c >= 14 && c <= 31 || c == 39 || c == 60 || c == 62) - sb.AppendFormat ("\\u{0:x4}", (int)c); - else switch ((int)c) { - case 8: - sb.Append ("\\b"); - break; - - case 9: - sb.Append ("\\t"); - break; - - case 10: - sb.Append ("\\n"); - break; - - case 12: - sb.Append ("\\f"); - break; - - case 13: - sb.Append ("\\r"); - break; - - case 34: - sb.Append ("\\\""); - break; - - case 92: - sb.Append ("\\\\"); - break; - - default: - sb.Append (c); - break; - } - } - - if (addDoubleQuotes) - sb.Append ('"'); - - return sb.ToString (); - } -#endif - public static string UrlPathEncode(string s) - { -#if NET_4_0 - return HttpEncoder.Current.UrlPathEncode (s); -#else - return HttpEncoder.UrlPathEncode(s); -#endif - } - - public static NameValueCollection ParseQueryString(string query) - { - return ParseQueryString(query, Encoding.UTF8); - } - - public static NameValueCollection ParseQueryString(string query, Encoding encoding) - { - if (query == null) - throw new ArgumentNullException("query"); - if (encoding == null) - throw new ArgumentNullException("encoding"); - if (query.Length == 0 || (query.Length == 1 && query[0] == '?')) - return new NameValueCollection(); - if (query[0] == '?') - query = query.Substring(1); - - NameValueCollection result = new HttpQSCollection(); - ParseQueryString(query, encoding, result); - return result; - } - - internal static void ParseQueryString(string query, Encoding encoding, NameValueCollection result) - { - if (query.Length == 0) - return; - - string decoded = HtmlDecode(query); - int decodedLength = decoded.Length; - int namePos = 0; - bool first = true; - while (namePos <= decodedLength) - { - int valuePos = -1, valueEnd = -1; - for (int q = namePos; q < decodedLength; q++) - { - if (valuePos == -1 && decoded[q] == '=') - { - valuePos = q + 1; - } - else if (decoded[q] == '&') - { - valueEnd = q; - break; - } - } - - if (first) - { - first = false; - if (decoded[namePos] == '?') - namePos++; - } - - string name, value; - if (valuePos == -1) - { - name = null; - valuePos = namePos; - } - else - { - name = UrlDecode(decoded.Substring(namePos, valuePos - namePos - 1), encoding); - } - if (valueEnd < 0) - { - namePos = -1; - valueEnd = decoded.Length; - } - else - { - namePos = valueEnd + 1; - } - value = UrlDecode(decoded.Substring(valuePos, valueEnd - valuePos), encoding); - - result.Add(name, value); - if (namePos == -1) - break; - } - } - #endregion // Methods - } -} 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 f7dca23c3..0f4ce6313 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.10.6.0 + 0.1100.0.4 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 6751608a6..784b6f961 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.10.6.0 + 0.1100.0.4 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index ff702b021..33ca74fff 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.10.6.0 + 0.1100.0.4 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index a3a8d8f9f..6e5118dfc 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.0 + 0.1100.0.4 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index a9341acb2..227414706 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.0 + 0.1100.0.4 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index e80aa3d4a..0080258ba 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -420,6 +420,7 @@ namespace Barotrauma if (writeStatus) { WriteStatus(tempBuffer); + (AIController as EnemyAIController)?.PetBehavior?.ServerWrite(tempBuffer); HealthUpdatePending = false; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index 3db355286..a5b30f5d8 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -2155,6 +2155,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..21ce1491d --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/ReadyCheck.cs @@ -0,0 +1,118 @@ +#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() + { + 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..176ca9a8d 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,7 @@ 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); } } } 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..fc0b06a65 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs @@ -238,7 +238,9 @@ namespace Barotrauma public void WriteSpawnData(IWriteMessage msg, UInt16 entityID, UInt16 originalInventoryID, byte originalItemContainerIndex) { - if (GameMain.Server == null) return; + if (GameMain.Server == null) { return; } + + int initialLength = msg.LengthBytes; msg.Write(Prefab.OriginalName); msg.Write(Prefab.Identifier); @@ -282,9 +284,21 @@ 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 ?? ""); } + if (msg.LengthBytes - initialLength >= 255) + { + DebugConsole.ThrowError($"Too much data in an item spawn message. Item: \"{Prefab.Identifier}\", msg bytes: {(msg.LengthBytes - initialLength)}, description changed: {(Description != prefab.Description)}, description: {Description}, tags changed: {tagsChanged}, tags: {Tags}"); + } } partial void UpdateNetPosition(float deltaTime) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs new file mode 100644 index 000000000..c8e780d8b --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Map/Creatures/BallastFloraBehavior.cs @@ -0,0 +1,77 @@ +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) + { + msg.Write(itemID); + msg.Write(infect); + } + + 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..74876a5ec 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,37 @@ 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: + behavior.ServerWriteInfect(message, itemID, infect); + 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..be1bb278a 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) { @@ -2130,6 +2133,7 @@ namespace Barotrauma.Networking Level.Loaded?.SpawnNPCs(); Level.Loaded?.SpawnCorpses(); + Level.Loaded?.PrepareBeaconStation(); AutoItemPlacer.PlaceIfNeeded(); CrewManager crewManager = campaign?.CrewManager; @@ -2253,7 +2257,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 +2273,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 +2284,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]); @@ -3662,7 +3666,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/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/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/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 18ebf43eb..f210efa42 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.0 + 0.1100.0.4 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 7765aec9d..0806a7007 100644 --- a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml +++ b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml @@ -99,7 +99,8 @@ - + + @@ -144,7 +145,7 @@ - + @@ -152,7 +153,27 @@ - + + + + + + + + + + + + + + + + + + + + + @@ -210,4 +231,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 796ff7163..281722ee5 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; } @@ -223,6 +226,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 +438,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 +475,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 +487,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 +616,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 +629,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 +688,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 +882,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 +1042,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 +1189,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 +1253,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 +1265,7 @@ namespace Barotrauma State = AIState.Idle; return; } - + if (AttackingLimb != null && AttackingLimb.attack.Retreat) { UpdateFallBack(attackWorldPos, deltaTime, false); @@ -1250,6 +1325,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 +1362,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 +1402,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 +1413,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,7 +1513,7 @@ 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) @@ -1450,13 +1556,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); @@ -1664,14 +1770,15 @@ namespace Barotrauma item.body.LinearVelocity *= 0.9f; item.body.LinearVelocity -= limbDiff * 0.25f; + bool wasBroken = item.Condition <= 0.0f; + item.AddDamage(Character, item.WorldPosition, new Attack(0.0f, 0.0f, 0.0f, 0.0f, 0.1f), deltaTime); if (item.Condition <= 0.0f) { + if (!wasBroken) { PetBehavior?.OnEat(item.GetTags(), 1.0f); } Entity.Spawner.AddToRemoveQueue(item); } - - PetBehavior?.OnEat(item.GetTags(), 0.1f / item.MaxCondition); } } } @@ -2028,6 +2135,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) @@ -2036,10 +2145,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; } @@ -2193,7 +2299,7 @@ namespace Barotrauma if (releaseTarget) { wallTarget = null; - LatchOntoAI.DeattachFromBody(cooldown: 1); + LatchOntoAI.DeattachFromBody(reset: true, cooldown: 1); } } else @@ -2407,7 +2513,7 @@ namespace Barotrauma protected override void OnStateChanged(AIState from, AIState to) { - LatchOntoAI?.DeattachFromBody(); + LatchOntoAI?.DeattachFromBody(reset: true); Character.AnimController.ReleaseStuckLimbs(); escapeTarget = null; AttackingLimb = null; @@ -2448,14 +2554,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) { @@ -2502,7 +2609,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; @@ -2545,6 +2652,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..8318de61d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -38,11 +38,49 @@ namespace Barotrauma private float respondToAttackTimer; private const float RespondToAttackInterval = 1.0f; + 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 +110,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 +126,6 @@ namespace Barotrauma partial void InitProjSpecific(); - private bool freezeAI; - public override void Update(float deltaTime) { if (DisableCrewAI || Character.Removed) { return; } @@ -176,15 +183,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 +223,9 @@ namespace Barotrauma outsideSteering.Reset(); } steeringManager = outsideSteering; + steeringBuffer = minSteeringBuffer; } + steeringBuffer = Math.Clamp(steeringBuffer, minSteeringBuffer, maxSteeringBuffer); AnimController.Crouching = shouldCrouch; CheckCrouching(deltaTime); @@ -369,7 +401,7 @@ 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.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 +467,7 @@ namespace Barotrauma if (oxygenLow || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority) { divingSuit.Drop(Character); + HandleRelocation(divingSuit); } else if (findItemState == FindItemState.None || findItemState == FindItemState.DivingSuit) { @@ -461,6 +494,7 @@ namespace Barotrauma else { divingSuit.Drop(Character); + HandleRelocation(divingSuit); } } } @@ -478,6 +512,7 @@ namespace Barotrauma if (ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority) { mask.Drop(Character); + HandleRelocation(mask); } else if (findItemState == FindItemState.None || findItemState == FindItemState.DivingMask) { @@ -501,6 +536,7 @@ namespace Barotrauma else { mask.Drop(Character); + HandleRelocation(mask); } } } @@ -548,6 +584,7 @@ namespace Barotrauma else { item.Drop(Character); + HandleRelocation(item); } } } @@ -556,6 +593,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 +678,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; } @@ -1223,6 +1317,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 +1338,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)) { @@ -1464,7 +1564,16 @@ 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.Where(it => it.CurrentHull == hull)) + { + 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 26cd8a304..3299ec59d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs @@ -192,13 +192,43 @@ namespace Barotrauma pathFinder.InsideSubmarine = character.Submarine != null; pathFinder.ApplyPenaltyToOutsideNodes = character.PressureProtection <= 0; var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", startNodeFilter, endNodeFilter, nodeFilter, checkVisibility: checkVisibility); - bool useNewPath = needsNewPath || currentPath == null || currentPath.CurrentNode == null || findPathTimer < -1; + bool useNewPath = needsNewPath || currentPath == null || currentPath.CurrentNode == null || findPathTimer < -1 && Math.Abs(character.AnimController.TargetMovement.X) <= 0; if (!useNewPath && currentPath != null && currentPath.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable) { - // It's possible that the current path was calculated from a start point that is no longer valid. - // Therefore, let's accept also paths with a greater cost than the current, if the current node is much farther than the new start node. - useNewPath = newPath.Cost < currentPath.Cost || - Vector2.DistanceSquared(character.WorldPosition, currentPath.CurrentNode.WorldPosition) > Math.Pow(Vector2.Distance(character.WorldPosition, newPath.Nodes.First().WorldPosition) * 3, 2); + // Check if the new path is the same as the old, in which case we just ignore it and continue using the old path (or the progress would reset). + if (IsIdenticalPath()) + { + useNewPath = false; + } + else + { + // Use the new path if it has significantly lower cost (don't change the path if it has marginally smaller cost. This reduces navigating backwards due to new path that is calculated from the node just behind us). + float t = (float)currentPath.CurrentIndex / (currentPath.Nodes.Count - 1); + useNewPath = newPath.Cost < currentPath.Cost * MathHelper.Lerp(0.95f, 0, t); + if (!useNewPath) + { + // It's possible that the current path was calculated from a start point that is no longer valid. + // Therefore, let's accept also paths with a greater cost than the current, if the current node is much farther than the new start node. + useNewPath = Vector2.DistanceSquared(character.WorldPosition, currentPath.CurrentNode.WorldPosition) > Math.Pow(Vector2.Distance(character.WorldPosition, newPath.Nodes.First().WorldPosition) * 3, 2); + } + } + + bool IsIdenticalPath() + { + int nodeCount = newPath.Nodes.Count; + if (nodeCount == currentPath.Nodes.Count) + { + for (int i = 0; i < nodeCount - 1; i++) + { + if (newPath.Nodes[i] != currentPath.Nodes[i]) + { + return false; + } + } + return true; + } + return false; + } } if (useNewPath) { @@ -407,7 +437,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) { @@ -689,7 +719,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..91bc0a874 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs @@ -15,15 +15,16 @@ namespace Barotrauma private float raycastTimer; - private Body attachTargetBody; + private Structure targetWall; + private Body targetBody; private Vector2 attachSurfaceNormal; - private Submarine attachTargetSubmarine; + private Submarine targetSubmarine; 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 +36,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 +45,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)); @@ -84,10 +77,11 @@ namespace Barotrauma 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; + targetWall = wall; + targetBody = wall.Submarine.PhysicsBody.FarseerBody; + targetSubmarine = wall.Submarine; this.attachSurfaceNormal = attachSurfaceNormal; wallAttachPos = attachPos; } @@ -98,28 +92,27 @@ namespace Barotrauma 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 +128,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) { @@ -168,7 +161,7 @@ namespace Barotrauma if (MathUtils.GetLineIntersection(edge.Point1, edge.Point2, character.WorldPosition, cell.Center, out Vector2 intersection)) { attachSurfaceNormal = edge.GetNormal(cell); - attachTargetBody = cell.Body; + targetBody = cell.Body; Vector2 potentialAttachPos = ConvertUnits.ToSimUnits(intersection); float distSqr = Vector2.DistanceSquared(character.SimPosition, wallAttachPos); if (distSqr < closestDist) @@ -192,7 +185,7 @@ namespace Barotrauma if (wallAttachPos == Vector2.Zero) { - DeattachFromBody(); + DeattachFromBody(reset: false); } else { @@ -201,13 +194,13 @@ namespace Barotrauma if (squaredDistance < targetDistance * targetDistance) { //close enough to a wall -> attach - AttachToBody(character.AnimController.Collider, attachLimb, attachTargetBody, wallAttachPos); + AttachToBody(character.AnimController.Collider, attachLimb, targetBody, 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,44 +210,60 @@ 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(character.AnimController.Collider, attachLimb, targetBody, 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; } } @@ -263,11 +272,11 @@ namespace Barotrauma { if (attachCooldown > 0) { return; } //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 +299,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 +318,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/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..c6d12d979 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; @@ -58,12 +59,10 @@ 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.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,28 @@ namespace Barotrauma return false; } } - return item.Prefab.PreferredContainers.Any(); + if (item.Prefab.PreferredContainers.None()) + { + return false; + } + bool canEquip = true; + if (!item.AllowedSlots.Contains(InvSlotType.Any)) + { + canEquip = false; + foreach (var allowedSlot in item.AllowedSlots) + { + int slot = character.Inventory.FindLimbSlot(allowedSlot); + if (slot > -1) + { + if (character.Inventory.Items[slot] == null) + { + canEquip = true; + break; + } + } + } + } + return canEquip; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs index cde9a0e2e..e71b389f8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs @@ -59,7 +59,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; @@ -86,7 +86,7 @@ namespace Barotrauma protected override void Act(float deltaTime) { - if (container == null) + if (container == null || (container.Item != null && container.Item.IsThisOrAnyContainerIgnoredByAI())) { Abandon = true; return; @@ -94,6 +94,11 @@ namespace Barotrauma Item itemToContain = item ?? character.Inventory.FindItem(i => CheckItem(i) && i.Container != container.Item, recursive: true); if (itemToContain != null) { + if (!character.CanInteractWith(itemToContain)) + { + Abandon = true; + return; + } if (character.CanInteractWith(container.Item, out _, checkLinked: false)) { if (RemoveEmpty) @@ -142,11 +147,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..defc6fc18 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs @@ -79,11 +79,17 @@ namespace Barotrauma TryAddSubObjective(ref getExtinguisherObjective, () => { character.Speak(TextManager.Get("DialogFindExtinguisher"), null, 2.0f, "findextinguisher", 30.0f); - return new AIObjectiveGetItem(character, "fireextinguisher", objectiveManager, equip: true) + 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 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..9ffd134f4 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 }; 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 25a2b3634..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); @@ -324,13 +338,15 @@ namespace Barotrauma Func nodeFilter = null; if (isInside && !AllowGoingOutside) { - nodeFilter = node => node.Waypoint.CurrentHull != null; + nodeFilter = n => n.Waypoint.CurrentHull != null; } - PathSteering.SteeringSeek(character.GetRelativeSimPosition(Target), 1, n => - { - if (n.Waypoint.isObstructed) { return false; } - return (n.Waypoint.CurrentHull == null) == (character.CurrentHull == null); - }, endNodeFilter, nodeFilter, CheckVisibility); + + PathSteering.SteeringSeek(character.GetRelativeSimPosition(Target), 1, + startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (character.CurrentHull == null), + endNodeFilter, + nodeFilter, + CheckVisibility); + if (!isInside && PathSteering.CurrentPath == null || PathSteering.IsPathDirty || PathSteering.CurrentPath.Unreachable) { if (useScooter) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs index 87bca211c..da1b8e492 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; @@ -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); 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 088906058..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)); } } @@ -92,6 +83,7 @@ namespace Barotrauma public GetNodePenaltyHandler GetNodePenalty; private readonly List nodes; + public readonly bool IndoorsSteering; public bool InsideSubmarine { get; set; } public bool ApplyPenaltyToOutsideNodes { get; set; } @@ -105,7 +97,7 @@ namespace Barotrauma wp.linkedTo.CollectionChanged += WaypointLinksChanged; } - InsideSubmarine = indoorsSteering; + IndoorsSteering = indoorsSteering; } void WaypointLinksChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) @@ -201,11 +193,13 @@ namespace Barotrauma if (nodeFilter != null && !nodeFilter(node)) { continue; } if (startNodeFilter != null && !startNodeFilter(node)) { continue; } //if searching for a path inside the sub, make sure the waypoint is visible - if (InsideSubmarine) + if (IndoorsSteering) { + if (node.Waypoint.isObstructed) { continue; } + // Always check the visibility for the start node var body = Submarine.PickBody( - start, node.TempPosition, null, + start, node.TempPosition, null, Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionStairs); if (body != null) { @@ -259,17 +253,20 @@ namespace Barotrauma { if (nodeFilter != null && !nodeFilter(node)) { continue; } if (endNodeFilter != null && !endNodeFilter(node)) { continue; } - - //if searching for a path inside the sub, make sure the waypoint is visible - if (InsideSubmarine && checkVisibility) + if (IndoorsSteering) { - // Only check the visibility for the end node when allowed (fix leaks) - var body = Submarine.PickBody(end, node.TempPosition, null, - Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionStairs ); - if (body != null) + if (node.Waypoint.isObstructed) { continue; } + //if searching for a path inside the sub, make sure the waypoint is visible + if (checkVisibility) { - if (body.UserData is Structure && !((Structure)body.UserData).IsPlatform) { continue; } - if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { continue; } + // Only check the visibility for the end node when allowed (fix leaks) + var body = Submarine.PickBody(end, node.TempPosition, null, + Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionStairs); + if (body != null) + { + if (body.UserData is Structure && !((Structure)body.UserData).IsPlatform) { continue; } + if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { continue; } + } } } endNode = node; @@ -343,6 +340,7 @@ namespace Barotrauma foreach (PathNode node in nodes) { if (node.state != 1) { continue; } + if (IndoorsSteering && node.Waypoint.isObstructed) { continue; } if (filter != null && !filter(node)) { continue; } if (node.F < dist) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs index 08c483e53..90c20a4f9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs @@ -1,5 +1,6 @@ using Barotrauma.Extensions; using Barotrauma.Items.Components; +using Barotrauma.Networking; using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Globalization; @@ -18,8 +19,19 @@ namespace Barotrauma Hungry } - public float Hunger { get; set; } = 50.0f; - public float Happiness { get; set; } = 50.0f; + private float hunger = 50.0f; + public float Hunger + { + get { return hunger; } + set { hunger = MathHelper.Clamp(value, 0.0f, MaxHunger); } + } + + private float happiness = 50.0f; + public float Happiness + { + get { return happiness; } + set { happiness = MathHelper.Clamp(value, 0.0f, MaxHappiness); } + } public float MaxHappiness { get; set; } public float MaxHunger { get; set; } @@ -86,9 +98,10 @@ namespace Barotrauma switch (subElement.Name.LocalName.ToLowerInvariant()) { case "item": + string identifier = subElement.GetAttributeString("identifier", ""); Item newItemToProduce = new Item { - Prefab = ItemPrefab.Find("", subElement.GetAttributeString("identifier", "")), + Prefab = string.IsNullOrEmpty(identifier) ? null : ItemPrefab.Find("", subElement.GetAttributeString("identifier", "")), Commonness = subElement.GetAttributeFloat("commonness", 0.0f) }; totalCommonness += newItemToProduce.Commonness; @@ -119,7 +132,7 @@ namespace Barotrauma for (int i = 0; i < Items.Count; i++) { aggregate += Items[i].Commonness; - if (aggregate >= r) + if (aggregate >= r && Items[i].Prefab != null) { Entity.Spawner.AddToSpawnQueue(Items[i].Prefab, pet.AiController.Character.WorldPosition); break; @@ -192,25 +205,21 @@ namespace Barotrauma public StatusIndicatorType GetCurrentStatusIndicatorType() { if (Hunger > MaxHunger * 0.5f) { return StatusIndicatorType.Hungry; } - if (Happiness > MaxHappiness * 0.75f) { return StatusIndicatorType.Happy; } + if (Happiness > MaxHappiness * 0.8f) { return StatusIndicatorType.Happy; } if (Happiness < MaxHappiness * 0.25f) { return StatusIndicatorType.Sad; } return StatusIndicatorType.None; } - public void OnEat(IEnumerable tags, float amount) + public bool OnEat(IEnumerable tags, float amount) { - for (int i = 0; i < foods.Count; i++) + foreach (string tag in tags) { - if (tags.Any(t => t.Equals(foods[i].Tag, System.StringComparison.OrdinalIgnoreCase))) - { - Hunger += foods[i].Hunger * amount; - Happiness += foods[i].Happiness * amount; - break; - } + if (OnEat(tag, amount)) { return true; } } + return false; } - public void OnEat(string tag, float amount) + public bool OnEat(string tag, float amount) { for (int i = 0; i < foods.Count; i++) { @@ -218,9 +227,13 @@ namespace Barotrauma { Hunger += foods[i].Hunger * amount; Happiness += foods[i].Happiness * amount; - break; +#if CLIENT + AiController.Character.PlaySound(CharacterSound.SoundType.Happy, 0.5f); +#endif + return true; } } + return false; } public void Play(Character player) @@ -230,9 +243,11 @@ namespace Barotrauma PlayTimer = 5.0f; AiController.Character.IsRagdolled = true; Happiness += 10.0f; - if (Happiness > MaxHappiness) { Happiness = MaxHappiness; } AiController.Character.AnimController.MainLimb.body.LinearVelocity += new Vector2(0, PlayForce); unstunY = AiController.Character.SimPosition.Y; +#if CLIENT + AiController.Character.PlaySound(CharacterSound.SoundType.Happy, 0.9f); +#endif } public string GetTagName() @@ -259,15 +274,6 @@ namespace Barotrauma { var character = AiController.Character; if (character?.Removed ?? true || character.IsDead) { return; } - if (GameMain.NetworkMember?.IsClient ?? false) { return; } - - if (Owner != null && (Owner.Removed || Owner.IsDead)) { Owner = null; } - - Hunger += HungerIncreaseRate * deltaTime; - - Happiness -= HappinessDecreaseRate * deltaTime; - - PlayTimer -= deltaTime; if (unstunY.HasValue) { @@ -292,6 +298,14 @@ namespace Barotrauma } } + PlayTimer -= deltaTime; + + if (GameMain.NetworkMember?.IsClient ?? false) { return; } + if (Owner != null && (Owner.Removed || Owner.IsDead)) { Owner = null; } + + Hunger += HungerIncreaseRate * deltaTime; + Happiness -= HappinessDecreaseRate * deltaTime; + for (int i = 0; i < foods.Count; i++) { Food food = foods[i]; @@ -311,12 +325,6 @@ namespace Barotrauma } } - if (Hunger < 0.0f) { Hunger = 0.0f; } - if (Hunger > MaxHunger) { Hunger = MaxHunger; } - if (Happiness < 0.0f) { Happiness = 0.0f; } - if (Happiness > MaxHappiness) { Happiness = MaxHappiness; } - if (PlayTimer < 0.0f) { PlayTimer = 0.0f; } - if (Hunger >= MaxHunger * 0.99f) { character.CharacterHealth.ApplyAffliction(character.AnimController.MainLimb, new Affliction(AfflictionPrefab.InternalDamage, 8.0f * deltaTime)); @@ -343,7 +351,7 @@ namespace Barotrauma foreach (Character c in Character.CharacterList) { if (!c.IsPet || c.IsDead) { continue; } - if (c.Submarine?.Info.Type != SubmarineType.Player) { continue; } + if (c.Submarine == null) { continue; } var petBehavior = (c.AIController as EnemyAIController)?.PetBehavior; if (petBehavior == null) { continue; } @@ -396,14 +404,32 @@ 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); + } } } + + public void ServerWrite(IWriteMessage msg) + { + msg.WriteRangedSingle(Happiness, 0.0f, MaxHappiness, 8); + msg.WriteRangedSingle(Hunger, 0.0f, MaxHunger, 8); + } + + public void ClientRead(IReadMessage msg) + { + Happiness = msg.ReadRangedSingle(0.0f, MaxHappiness, 8); + Hunger = msg.ReadRangedSingle(0.0f, MaxHunger, 8); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs index c583b6dcf..579a658e2 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(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs index ddfa90aea..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) { @@ -703,7 +704,7 @@ namespace Barotrauma if (head != null) { bool headFacingBackwards = false; - if (HeadAngle.HasValue) + if (HeadAngle.HasValue && head != mainLimb) { SmoothRotateWithoutWrapping(head, movementAngle + HeadAngle.Value * Dir, mainLimb, HeadTorque); if (Math.Sign(head.SimPosition.X - mainLimb.SimPosition.X) != Math.Sign(Dir)) @@ -853,11 +854,35 @@ namespace Barotrauma float noise = (PerlinNoise.GetPerlin(WalkPos * 0.002f, WalkPos * 0.003f) - 0.5f) * 5.0f; float animStrength = (1.0f - deathAnimTimer / deathAnimDuration); - Limb head = GetLimb(LimbType.Head); - if (head != null && head.IsSevered) { return; } + Limb baseLimb = GetLimb(LimbType.Head); + //if head is the main limb, it technically can't be severed - the rest of the limbs are considered severed if the head gets cut off + if (baseLimb == MainLimb) + { + int connectedToHeadCount = GetConnectedLimbs(baseLimb).Count; + //if there's nothing connected to the head, don't make it wiggle by itself + if (connectedToHeadCount == 1) { baseLimb = null; } + Limb torso = GetLimb(LimbType.Torso, excludeSevered: false); + if (torso != null) + { + //if there are more limbs connected to the torso than to the head, make the torso wiggle instead + int connectedToTorsoCount = GetConnectedLimbs(torso).Count; + if (connectedToTorsoCount > connectedToHeadCount) + { + baseLimb = torso; + } + } + } + else if (baseLimb == null) + { + baseLimb = GetLimb(LimbType.Torso, excludeSevered: true); + if (baseLimb == null) { return; } + } + + var connectedToBaseLimb = GetConnectedLimbs(baseLimb); + Limb tail = GetLimb(LimbType.Tail); - if (head != null && !head.IsSevered) head.body.ApplyTorque((float)(Math.Sqrt(head.Mass) * Dir * (Math.Sin(WalkPos) + noise)) * 30.0f * animStrength); - if (tail != null && !tail.IsSevered) tail.body.ApplyTorque((float)(Math.Sqrt(tail.Mass) * -Dir * (Math.Sin(WalkPos) + noise)) * 30.0f * animStrength); + if (baseLimb != null) { baseLimb.body.ApplyTorque((float)(Math.Sqrt(baseLimb.Mass) * Dir * (Math.Sin(WalkPos) + noise)) * 30.0f * animStrength); } + if (tail != null && connectedToBaseLimb.Contains(tail)) { tail.body.ApplyTorque((float)(Math.Sqrt(tail.Mass) * -Dir * (Math.Sin(WalkPos) + noise)) * 30.0f * animStrength); } WalkPos += deltaTime * 10.0f * animStrength; @@ -865,7 +890,7 @@ namespace Barotrauma foreach (Limb limb in Limbs) { - if (limb.IsSevered) { continue; } + if (!connectedToBaseLimb.Contains(limb)) { continue; } #if CLIENT if (limb.LightSource != null) { 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 c4855b08a..11f3f3f3a 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; } } @@ -782,6 +782,14 @@ namespace Barotrauma partial void SeverLimbJointProjSpecific(LimbJoint limbJoint, bool playSound); + protected List GetConnectedLimbs(Limb limb) + { + connectedLimbs.Clear(); + checkedJoints.Clear(); + GetConnectedLimbs(connectedLimbs, checkedJoints, limb); + return connectedLimbs; + } + private void GetConnectedLimbs(List connectedLimbs, List checkedJoints, Limb limb) { connectedLimbs.Add(limb); @@ -1052,7 +1060,7 @@ namespace Barotrauma foreach (Limb limb in Limbs) { - if (limb.ignoreCollisions || limb.IsSevered) { continue; } + if (limb.IgnoreCollisions || limb.IsSevered) { continue; } try { @@ -1626,7 +1634,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; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index 369b0fec5..5b2607e9d 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,24 @@ 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(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. /// @@ -379,7 +392,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 +416,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 +437,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 +456,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 +480,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 +497,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 7ecb98a09..ce95510de 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,16 +758,24 @@ 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)) { speciesName = Path.GetFileNameWithoutExtension(speciesName).ToLowerInvariant(); } + + if (CharacterPrefab.FindBySpeciesName(speciesName) == null) + { + DebugConsole.ThrowError($"Failed to create character \"{speciesName}\". Matching prefab not found.\n" + Environment.StackTrace); + return null; + } + Character newCharacter = null; if (!speciesName.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase)) { @@ -783,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; @@ -823,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); @@ -1461,7 +1471,7 @@ namespace Barotrauma AnimController.ReleaseStuckLimbs(); if (AIController != null && AIController is EnemyAIController enemyAI) { - enemyAI.LatchOntoAI?.DeattachFromBody(); + enemyAI.LatchOntoAI?.DeattachFromBody(reset: true); } } #endif @@ -1517,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; } @@ -1629,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) @@ -2919,7 +2931,7 @@ namespace Barotrauma } #endif // Don't allow beheading for monster attacks, because it happens too frequently (crawlers/tigerthreshers etc attacking each other -> they will most often target to the head) - TrySeverLimbJoints(limbHit, attack.SeverLimbsProbability, attackResult.Damage, allowBeheading: attacker.IsHuman || attacker.IsPlayer); + TrySeverLimbJoints(limbHit, attack.SeverLimbsProbability, attackResult.Damage, allowBeheading: attacker == null || attacker.IsHuman || attacker.IsPlayer); return attackResult; } @@ -2962,7 +2974,7 @@ namespace Barotrauma if (severed) { Limb otherLimb = joint.LimbA == targetLimb ? joint.LimbB : joint.LimbA; - otherLimb.body.ApplyLinearImpulse(targetLimb.LinearVelocity * targetLimb.Mass); + otherLimb.body.ApplyLinearImpulse(targetLimb.LinearVelocity * targetLimb.Mass, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.5f); ApplyStatusEffects(ActionType.OnSevered, 1.0f); targetLimb.ApplyStatusEffects(ActionType.OnSevered, 1.0f); otherLimb.ApplyStatusEffects(ActionType.OnSevered, 1.0f); @@ -3423,6 +3435,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 68e537345..aa4e3ce3a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -151,19 +151,21 @@ namespace Barotrauma public XElement HealthData; private static ushort idCounter; + private const string disguiseName = "???"; public string Name; public string DisplayName { get { - string disguiseName = "?"; 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..71ca28a0d 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 { @@ -1160,8 +1161,10 @@ 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); + } } }, null, isCheat: true)); @@ -1259,6 +1262,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)); 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/EventActions/RNGAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RNGAction.cs index 148ef7e9c..f08ec8423 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RNGAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/RNGAction.cs @@ -9,7 +9,17 @@ namespace Barotrauma [Serialize(0.0f, true)] public float Chance { get; set; } - public RNGAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { } + public RNGAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) + { + if (Chance >= 1.0f) + { + DebugConsole.ThrowError($"Incorrectly configured RNG Action in event \"{parentEvent.Prefab.Identifier}\". Probability is 1.0 (100%) or more, the action will always succeed."); + } + else if (Chance <= 0.0f) + { + DebugConsole.ThrowError($"Incorrectly configured RNG Action in event \"{parentEvent.Prefab.Identifier}\". Probability is 0 or less, the action will never succeed."); + } + } private bool isFinished; 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..d3f99cab7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs @@ -123,7 +123,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); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index 9fa16424f..78d145b41 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++) { @@ -371,7 +370,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 @@ -390,7 +389,7 @@ namespace Barotrauma foreach (EventSet childEventSet in eventSet.ChildSets) { - CreateEvents(childEventSet); + CreateEvents(childEventSet, rand); } } } @@ -583,7 +582,7 @@ namespace Barotrauma enemyDanger = 0.0f; foreach (Character character in Character.CharacterList) { - if (character.IsDead || character.IsIncapacitated || !character.Enabled) continue; + if (character.IsDead || character.IsIncapacitated || !character.Enabled || character.IsPet || character.Params.CompareGroup("human")) { continue; } EnemyAIController enemyAI = character.AIController as EnemyAIController; if (enemyAI == null) continue; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs new file mode 100644 index 000000000..993129200 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/BeaconMission.cs @@ -0,0 +1,101 @@ +using Barotrauma.Items.Components; +using Barotrauma.Networking; +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 string monsterSpeciesName; + private Point monsterCountRange; + private Level level; + private Location[] locations; + private 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); + + this.locations = locations; + + 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; + int amount = Rand.Range(monsterCountRange.X, monsterCountRange.Y + 1); + for (int i = 0; i < amount; i++) + { + Entity.Spawner.AddToSpawnQueue(monsterSpeciesName, spawnPos); + } + swarmSpawned = true; + } + } + + public override void End() + { + completed = level.CheckBeaconActive(); + if (completed) + { + if (GameMain.GameSession.GameMode is CampaignMode) + { + int naturalFormationIndex = locations[0].Type.Identifier.Equals("None", StringComparison.OrdinalIgnoreCase) ? 0 : 1; + var upgradeLocation = locations[naturalFormationIndex]; + upgradeLocation.ChangeType(LocationType.List.Find(lt => lt.Identifier.Equals("Explored", StringComparison.OrdinalIgnoreCase))); + } + 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..50a863a02 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs @@ -0,0 +1,152 @@ +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 (IsClient) { return; } + foreach (var kvp in ResourceClusters) + { + var prefab = ItemPrefab.Find(null, kvp.Key); + if (prefab == null) { continue; } + var spawnedResources = level.GenerateMissionResources(prefab, kvp.Value.First, out float rotation); + 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()) { return; } + GiveReward(); + completed = true; + } + + 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..df06e5488 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,7 @@ namespace Barotrauma } } } + + 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..178611d32 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs @@ -96,7 +96,7 @@ namespace Barotrauma 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..9bcfdbc0b --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/NestMission.cs @@ -0,0 +1,278 @@ +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 + { + 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 (!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()) + { + return; + } + foreach (Item item in items) + { + if (item != null && !item.Removed) + { + item.Remove(); + } + } + items.Clear(); + GiveReward(); + completed = true; + failed = !completed && state > 0; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs index 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..f7f9c870c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs @@ -149,7 +149,11 @@ namespace Barotrauma continue; } } - if (position.PositionType != Level.PositionType.MainPath) { continue; } + if (position.PositionType != Level.PositionType.MainPath && + position.PositionType != Level.PositionType.SidePath) + { + continue; + } if (Level.Loaded.ExtraWalls.Any(w => w.Cells.Any(c => c.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/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..4101a63b7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs @@ -204,7 +204,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..d9e08f63a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -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(); 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/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs index b6f891ac0..37039ee37 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -150,6 +150,9 @@ namespace Barotrauma case "cargo": CargoManager?.LoadPurchasedItems(subElement); break; + case "pets": + petsElement = subElement; + break; #if SERVER case "availablesubs": foreach (XElement availableSub in subElement.Elements()) 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/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..c91d04396 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs @@ -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..4d2175f14 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Numerics; using System.Xml.Linq; using Barotrauma.Extensions; +using Barotrauma.MapCreatures.Behavior; using Barotrauma.Networking; using FarseerPhysics; using FarseerPhysics.Dynamics; @@ -138,19 +139,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 +173,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 +199,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 +213,7 @@ namespace Barotrauma.Items.Components } } - if (GrowthStep >= 2.0f || Parent.Decayed) { return; } + if (GrowthStep >= 2.0f || decayed) { return; } GrowthStep += deltaTime; @@ -289,6 +293,8 @@ namespace Barotrauma.Items.Components 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 +319,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 @@ -705,8 +716,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 +820,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 3cbf4caf7..1837d6e8d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -195,7 +195,9 @@ namespace Barotrauma.Items.Components } } } - } + } + + characterUsable = element.GetAttributeBool("characterusable", true); } private bool OnPusherCollision(Fixture sender, Fixture other, Contact contact) @@ -211,9 +213,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) { @@ -514,9 +516,10 @@ namespace Barotrauma.Items.Components public override bool Use(float deltaTime, Character character = null) { - if (!attachable || item.body == null) { return character == null || character.IsKeyDown(InputType.Aim); } + if (!attachable || item.body == null) { return character == null || (character.IsKeyDown(InputType.Aim) && characterUsable); } if (character != null) { + if (!characterUsable && !attachable) { return false; } if (!character.IsKeyDown(InputType.Aim)) { return false; } if (!CanBeAttached(character)) { return false; } 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..5bde257d5 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,7 +70,7 @@ 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; @@ -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/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..f4dca73d1 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) + { + 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/Holdable/Throwable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs index 4680287cd..ac2f0ac8b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs @@ -63,14 +63,14 @@ namespace Barotrauma.Items.Components if (item.body.LinearVelocity.LengthSquared() < 0.01f) { CurrentThrower = null; - if (statusEffectLists.ContainsKey(ActionType.OnImpact)) + if (statusEffectLists?.ContainsKey(ActionType.OnImpact) ?? false) { foreach (var statusEffect in statusEffectLists[ActionType.OnImpact]) { statusEffect.SetUser(null); } } - if (statusEffectLists.ContainsKey(ActionType.OnBroken)) + if (statusEffectLists?.ContainsKey(ActionType.OnBroken) ?? false) { foreach (var statusEffect in statusEffectLists[ActionType.OnBroken]) { @@ -135,14 +135,14 @@ namespace Barotrauma.Items.Components GameServer.Log(GameServer.CharacterLogName(picker) + " threw " + item.Name, ServerLog.MessageType.ItemInteraction); #endif CurrentThrower = picker; - if (statusEffectLists.ContainsKey(ActionType.OnImpact)) + if (statusEffectLists?.ContainsKey(ActionType.OnImpact) ?? false) { foreach (var statusEffect in statusEffectLists[ActionType.OnImpact]) { statusEffect.SetUser(CurrentThrower); } } - if (statusEffectLists.ContainsKey(ActionType.OnBroken)) + if (statusEffectLists?.ContainsKey(ActionType.OnBroken) ?? false) { foreach (var statusEffect in statusEffectLists[ActionType.OnBroken]) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index cddb7d1f2..29503cd75 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -763,7 +763,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) { @@ -1011,6 +1011,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..8ed2f963b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -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/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/Sonar.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs index 96d1daf63..9b2451498 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,9 @@ namespace Barotrauma.Items.Components set; } + [Editable, Serialize(false, false, description: "Does the sonar have mineral scanning mode?")] + public bool HasMineralScanner { get; set; } + public float Zoom { get { return zoom; } @@ -343,6 +347,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 +356,7 @@ namespace Barotrauma.Items.Components { pingDirectionT = msg.ReadRangedSingle(0.0f, 1.0f, 8); } + mineralScanner = msg.ReadBoolean(); } if (!item.CanClientAccess(c)) { return; } @@ -366,9 +372,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 +399,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..4dbeb06b4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs @@ -417,6 +417,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 +427,26 @@ namespace Barotrauma.Items.Components var closeCells = Level.Loaded.GetCells(controlledSub.WorldPosition, 4); foreach (VoronoiCell cell in closeCells) { + if (Level.Loaded?.ExtraWalls.Any(w => w.WallDamageOnTouch > 0.0f && w.Cells.Contains(cell)) ?? false) + { + 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) { 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 fed67f5c4..9205afa79 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,51 @@ namespace Barotrauma.Items.Components } } - public override bool Use(float deltaTime, Character character = null) + private void Launch(Character user, Vector2 simPosition, float rotation) + { + //User = user; + Item.body.ResetDynamics(); + Item.SetTransform(simPosition, rotation); + Use(); + if (Item.Removed) { return; } + User = user; + 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 +274,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(); @@ -360,6 +404,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 +426,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 +623,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 && 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 +679,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) { @@ -639,6 +692,7 @@ namespace Barotrauma.Items.Components } target.Body.ApplyLinearImpulse(velocity * item.body.Mass); + target.Body.LinearVelocity = target.Body.LinearVelocity.ClampLength(NetConfig.MaxPhysicsBodyVelocity * 0.5f); if (hits.Count() >= MaxTargetsToHit || hits.LastOrDefault()?.UserData is VoronoiCell) { @@ -702,7 +756,7 @@ namespace Barotrauma.Items.Components if (RemoveOnHit) { - Entity.Spawner.AddToRemoveQueue(item); + 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/Wire.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs index 38142a897..e32534946 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs @@ -63,7 +63,7 @@ namespace Barotrauma.Items.Components public bool Hidden; - private float removeNodeDelay; + private float editNodeDelay; private bool locked; public bool Locked @@ -200,14 +200,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; } @@ -290,7 +292,7 @@ namespace Barotrauma.Items.Components if (nodes.Count == 0) { return; } Character user = item.ParentInventory?.Owner as Character; - removeNodeDelay = (user?.SelectedConstruction == null) ? removeNodeDelay - deltaTime : 0.5f; + editNodeDelay = (user?.SelectedConstruction == null) ? editNodeDelay - deltaTime : 0.5f; Submarine sub = item.Submarine; if (connections[0] != null && connections[0].Item.Submarine != null) { sub = connections[0].Item.Submarine; } @@ -416,7 +418,9 @@ namespace Barotrauma.Items.Components #endif //clients communicate node addition/removal with network events if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { return false; } - if (newNodePos != Vector2.Zero && canPlaceNode && nodes.Count > 0 && Vector2.Distance(newNodePos, nodes[nodes.Count - 1]) > MinNodeDistance) + + if (newNodePos != Vector2.Zero && canPlaceNode && editNodeDelay <= 0.0f && nodes.Count > 0 && + Vector2.DistanceSquared(newNodePos, nodes[nodes.Count - 1]) > MinNodeDistance * MinNodeDistance) { if (nodes.Count >= MaxNodeCount) { @@ -440,6 +444,7 @@ namespace Barotrauma.Items.Components } #endif } + editNodeDelay = 0.1f; return true; } @@ -450,7 +455,7 @@ namespace Barotrauma.Items.Components //clients communicate node addition/removal with network events if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { return false; } - if (nodes.Count > 1 && removeNodeDelay <= 0.0f) + if (nodes.Count > 1 && editNodeDelay <= 0.0f) { nodes.RemoveAt(nodes.Count - 1); UpdateSections(); @@ -466,7 +471,7 @@ namespace Barotrauma.Items.Components } #endif } - removeNodeDelay = 0.1f; + editNodeDelay = 0.1f; Drawable = IsActive || sections.Count > 0; return true; @@ -617,8 +622,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(); } } } @@ -648,17 +653,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 @@ -747,9 +764,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/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..5828dcc47 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 @@ -626,6 +628,8 @@ namespace Barotrauma get { return Prefab.Linkable; } } + public BallastFloraBranch Infector { get; set; } + public override string ToString() { #if CLIENT @@ -640,14 +644,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 +661,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 +742,8 @@ namespace Barotrauma case "upgrademodule": case "upgradeoverride": case "minimapicon": + case "infectedsprite": + case "damagedinfectedsprite": break; case "staticbody": StaticBodyConfig = subElement; @@ -834,7 +841,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 +1180,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 +1444,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 +1477,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) @@ -2481,9 +2505,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 +2517,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 +2536,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 +2565,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 +2597,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..e37ac8fda 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -471,6 +471,9 @@ namespace Barotrauma [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 +597,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 +650,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 +773,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("/")) @@ -876,7 +900,7 @@ namespace Barotrauma case "levelresource": foreach (XElement levelCommonnessElement in subElement.Elements()) { - string levelName = levelCommonnessElement.GetAttributeString("levelname", "").ToLowerInvariant(); + string levelName = levelCommonnessElement.GetAttributeString("leveltype", "").ToLowerInvariant(); if (!LevelCommonness.ContainsKey(levelName)) { LevelCommonness.Add(levelName, levelCommonnessElement.GetAttributeFloat("commonness", 0.0f)); @@ -927,6 +951,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..10952ac54 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs @@ -0,0 +1,1091 @@ +#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; } + + private float availablePower; + + private float toxinsTimer; + + 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; + + 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"); + int health = getInt("health"); + int maxhealth = getInt("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); + } + } + + 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); + } + } + } + else + { + if (selfDamageTimer <= 0) + { + if (!CanGrowMore()) + { + foreach (BallastFloraBranch branch in Branches) + { + float maxHealth = branch.IsRoot ? StemHealth : BranchHealth; + DamageBranch(branch, Rand.Range(1f, maxHealth), AttackType.Other); + } + } + + selfDamageTimer = 1f; + } + + selfDamageTimer -= deltaTime; + } + + 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 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.Max(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); + } +#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("ArmorBreak", 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 + Vector2 pos = GetWorldPosition() + branch.Position; + + GameMain.ParticleManager.CreateParticle("bloodsplash", pos, Rand.Range(0, 360), Rand.Range(0, 100)); + GameMain.ParticleManager.CreateParticle("waterblood", pos, Rand.Range(0, 360), 0); + + for (int i = 0; i < 4; i++) + { + GameMain.ParticleManager.CreateParticle("gib", pos, Rand.Range(0, 360), Rand.Range(100f, 300f)); + } +#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("ArmorBreak", 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..e9be32c76 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/State/DefendWithPumpState.cs @@ -0,0 +1,120 @@ +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); + 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; + } + } + + public void Update(float deltaTime) + { + foreach (Pump pump in targetPumps) + { + if (pump.TargetLevel != null) + { + pump.TargetLevel = 100f; + } + else + { + pump.FlowPercentage = 100f; + } + } + + 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..37143041a 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,6 +32,8 @@ 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) { @@ -65,6 +69,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"); @@ -128,6 +133,11 @@ namespace Barotrauma RangedStructureDamage(worldPosition, displayRange, attack.GetStructureDamage(1.0f), attacker); } + if (BallastFloraDamage > 0.0f) + { + RangedBallastFloraDamage(worldPosition, displayRange, BallastFloraDamage, attacker); + } + if (EmpStrength > 0.0f) { float displayRangeSqr = displayRange * displayRange; @@ -167,8 +177,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 +191,9 @@ namespace Barotrauma while (container != null) { if (container.FireProof) - { - fireProof = true; - break; + { + fireProof = true; + break; } container = container.Container; } @@ -190,20 +204,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 +253,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) { continue; } float dist = Vector2.Distance(limb.WorldPosition, worldPosition); @@ -255,37 +267,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 +354,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, Character attacker = null, bool damageLevelWalls = true) { List structureList = new List(); float dist = 600.0f; @@ -395,23 +377,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 && damageLevelWalls) + { + 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(damage, 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..8ccd3ca54 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; } } @@ -137,18 +167,16 @@ namespace Barotrauma 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 +185,16 @@ 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 +209,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 +236,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); @@ -262,11 +264,35 @@ namespace Barotrauma 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); 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 +305,21 @@ 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); + edgeNormal * edgeLength * (roundingAmount + randomVariance) * centerF; + + //check if extruding the edge causes it to go inside another one + var nearbyCells = Level.Loaded.GetCells(extrudedPoint, searchDepth: 1); + if (!nearbyCells.Any(c => c.CellType == CellType.Solid && c != cell && c.IsPointInside(extrudedPoint))) { edgePoints.Add(extrudedPoint); } } } - 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 +328,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 }); } } @@ -348,14 +382,14 @@ namespace Barotrauma } renderTriangles.AddRange(MathUtils.TriangulateConvexHull(tempVertices, cell.Center)); - - if (bodyPoints.Count < 2) continue; + + 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,8 +400,8 @@ 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)); @@ -380,13 +414,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,10 +432,10 @@ 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.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..e681c8057 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/DestructibleLevelWall.cs @@ -0,0 +1,209 @@ +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); + } + + 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); + 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..863809161 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,9 +442,41 @@ 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); @@ -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,16 @@ 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.RemoveAll(c => c.Edges.Any(e => !MathUtils.NearlyEqual(e.Point1.Y, Size.Y) && e.AdjacentCell(c) == null)); 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 +677,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); } } @@ -563,11 +714,31 @@ namespace Barotrauma int x = (int)Math.Floor(cell.Site.Coord.X / GridCellSize); int y = (int)Math.Floor(cell.Site.Coord.Y / GridCellSize); - if (x < 0 || y < 0 || x >= cellGrid.GetLength(0) || y >= cellGrid.GetLength(1)) continue; + if (x < 0 || y < 0 || x >= cellGrid.GetLength(0) || y >= cellGrid.GetLength(1)) { continue; } 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 +748,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 +762,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 +794,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 +810,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 +928,7 @@ namespace Barotrauma bodies.Add(TopBarrier); - GenerateSeaFloor(mirror); + GenerateSeaFloor(); if (mirror) { @@ -694,21 +946,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 +1000,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 +1090,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 +1117,40 @@ 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); 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 +1159,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 +1241,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; } @@ -883,25 +1268,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 +1312,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 +1330,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 +1423,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 +1468,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 +1478,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 +1492,689 @@ 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) + { + 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; } + //don't spawn spires near the start/end of the level + if (cell.Center.X < Size.X * 0.2f || cell.Center.X > Size.X * 0.8f) { continue; } + foreach (GraphEdge edge in cell.Edges) + { + if (!edge.IsSolid || usedSpireEdges.Contains(edge) || edge.NextToCave) { continue; } + if (Vector2.DistanceSquared(edge.Point1, edge.Point2) > 1000.0f * 1000.0f) { continue; } + 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), 15000.0f); + Vector2 extrudedPoint1 = closestEdge.Point1 + edgeNormal * spireLength * Rand.Range(0.8f, 1.0f, Rand.RandSync.Server); + Vector2 extrudedPoint2 = closestEdge.Point2 + edgeNormal * spireLength * Rand.Range(0.8f, 1.0f, Rand.RandSync.Server); + List vertices = new List() + { + 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>(); 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)); } } DebugConsole.Log("Generating level resources..."); - for (int i = 0; i < GenerationParams.ItemCount; i++) + PathPoints.Clear(); + nextPathPointId = 0; + + foreach (Tunnel tunnel in Tunnels) { - 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) + var tunnelLength = 0.0f; + for (int i = 1; i < tunnel.Nodes.Count; i++) { - DebugConsole.ThrowError("Error while placing items in the level - item \"" + item.Name + "\" is not holdable and cannot be attached to the level walls."); + 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; + var allValidLocations = GetAllValidClusterLocations(); + string[] exclusiveResourceTags = new string[2] { "ore", "plant" }; + var maxResourceOverlap = 0.4f; + + // Create first cluster for each spawn point + foreach (var pathPoint in PathPoints.Where(p => p.ShouldContainResources)) + { + 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 edgeLength = Vector2.Distance(location.Edge.Point1, location.Edge.Point2); + var maxFitOnEdge = (int)Math.Floor(edgeLength / ((1.0f - maxResourceOverlap) * selectedPrefab.Size.X)); + 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; + } + } + + /// 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 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; + + bool IsValidEdge(GraphEdge e) + { + if (!e.IsSolid) { return false; } + if (e.OutsideLevel) { return false; } + return ExtraWalls.None(w => w.Cells.Any(c => c.IsPointInside(e.Center) || + c.IsPointInside(e.Center - 100 * e.GetNormal(c)) || + c.Edges.Any(extraWallEdge => extraWallEdge == e))); + } + } + + 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.Rect.Height / 2, ignoreContacts: true); + if (item.GetComponent() is Holdable h) + { + h.AttachToWall(); #if CLIENT item.Rotation = MathHelper.ToDegrees(-MathUtils.VectorToAngle(edgeNormal) + MathHelper.PiOver2); #endif } + placedResources.Add(item); } - - DebugConsole.Log("Level resources generated"); } public Vector2 GetRandomItemPos(PositionType spawnPosType, float randomSpread, float minDistFromSubs, float offsetFromWall = 10.0f) @@ -1343,7 +2189,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 +2202,8 @@ namespace Barotrauma if (Submarine.PickBody( ConvertUnits.ToSimUnits(startPos), ConvertUnits.ToSimUnits(endPos), - null, Physics.CollisionLevel | Physics.CollisionWall) != null) + ExtraWalls.Where(w => w.Body != null && w.Body.BodyType == BodyType.Dynamic).Select(w => w.Body), + Physics.CollisionLevel | Physics.CollisionWall) != null) { position = ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition) + Vector2.Normalize(startPos - endPos) * offsetFromWall; break; @@ -1374,7 +2221,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 +2238,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 +2292,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 +2376,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 +2386,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 +2455,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 +2762,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 +2968,97 @@ 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); + + Item sonarItem = Item.ItemList.Find(it => it.Submarine == BeaconStation && it.GetComponent() != null); + 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 +3164,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 +3207,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 +3226,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..f7be386df 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,6 +93,8 @@ namespace Barotrauma } } + InitialDepth = element.GetAttributeInt("initialdepth", GenerationParams.InitialDepthMin); + string biomeIdentifier = element.GetAttributeString("biome", ""); Biome = LevelGenerationParams.GetBiomes().FirstOrDefault(b => b.Identifier == biomeIdentifier); if (Biome == null) @@ -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,23 @@ 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( + float beaconRng = Rand.Range(0.0f, 1.0f, Rand.RandSync.Server); + var levelData = new LevelData( seed, difficulty ?? Rand.Range(30.0f, 80.0f, Rand.RandSync.Server), Rand.Range(0.0f, 1.0f, Rand.RandSync.Server), generationParams, - biome); + biome) + { + HasBeaconStation = beaconRng < 0.5f, + IsBeaconActive = beaconRng > 0.25f + }; + GameMain.GameSession?.GameMode?.Mission?.AdjustLevelData(levelData); + return levelData; } public void Save(XElement parentElement) @@ -156,7 +197,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..74b557453 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,6 +188,13 @@ namespace Barotrauma set; } + [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(MinValueInt = 10000, MaxValueInt = 1000000)] public int MinWidth { @@ -206,6 +216,20 @@ namespace Barotrauma set { height = Math.Max(value, 2000); } } + [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)] public int MinTunnelRadius { @@ -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..f5f6a9667 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) + availableSpawnPositions.Clear(); + foreach (Level.Cave cave in level.Caves) + { + availablePrefabs = new List(LevelObjectPrefab.List.FindAll(p => p.SpawnPos.HasFlag(LevelObjectPrefab.SpawnPosType.CaveWall))); + 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); @@ -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..68fba44da 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}'"); } - 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..dfa348e8a 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, damageLevelWalls: false); } } + + if (!string.IsNullOrWhiteSpace(InfectIdentifier)) + { + submarine.AttemptBallastFloraInfection(InfectIdentifier, deltaTime, InfectionChance); + } } if (Force.LengthSquared() > 0.01f) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelWall.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelWall.cs index 5fdb2ebec..e18495c94 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,8 @@ namespace Barotrauma } } + public float WallDamageOnTouch; + public float MoveSpeed; private Vector2? originalPos; @@ -50,30 +50,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 +86,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 +96,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 +125,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 +143,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..1e0bd3eea 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,21 +76,12 @@ 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; } + public int[] ProximityTime { get; private set; } + private const float StoreMaxReputationModifier = 0.1f; private const float StoreSellPriceModifier = 0.8f; private const float MechanicalMaxDiscountPercentage = 50.0f; @@ -191,6 +182,7 @@ namespace Barotrauma MapPosition = mapPosition; PortraitId = ToolBox.StringToInt(Name); Connections = new List(); + ProximityTime = new int[Type.CanChangeTo.Count]; } public Location(XElement element) @@ -200,10 +192,11 @@ 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); + ProximityTime = element.GetAttributeIntArray("proximitytime", new int[Type.CanChangeTo.Count]); + if (ProximityTime.Length != Type.CanChangeTo.Count) { ProximityTime = new int[Type.CanChangeTo.Count]; } MechanicalPriceMultiplier = element.GetAttributeFloat("mechanicalpricemultipler", 1.0f); string[] takenItemStr = element.GetAttributeStringArray("takenitems", new string[0]); @@ -285,6 +278,7 @@ namespace Barotrauma DebugConsole.Log("Location " + baseName + " changed it's type from " + Type + " to " + newType); Type = newType; + ProximityTime = new int[Type.CanChangeTo.Count]; Name = Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", baseName); CreateStore(force: true); } @@ -292,7 +286,8 @@ namespace Barotrauma public void UnlockMission(MissionPrefab missionPrefab, LocationConnection connection) { if (AvailableMissions.Any(m => m.Prefab == missionPrefab)) { return; } - availableMissions.Add(InstantiateMission(missionPrefab, connection)); + var mission = InstantiateMission(missionPrefab, ref connection); + availableMissions.Add(mission); #if CLIENT GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo(); #endif @@ -309,7 +304,8 @@ namespace Barotrauma } else { - var mission = InstantiateMission(missionPrefab); + LocationConnection connection = null; + var mission = InstantiateMission(missionPrefab, ref 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]))) { @@ -341,8 +337,9 @@ namespace Barotrauma { suitableMissions = unusedMissions; } + LocationConnection connection = null; MissionPrefab missionPrefab = suitableMissions.GetRandom(); - var mission = InstantiateMission(missionPrefab); + var mission = InstantiateMission(missionPrefab, ref 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 +360,23 @@ namespace Barotrauma return null; } - private Mission InstantiateMission(MissionPrefab prefab, LocationConnection connection = null) + private Mission InstantiateMission(MissionPrefab prefab, ref 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; } 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,9 +694,12 @@ 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)); + if (ProximityTime.Length > 0 && ProximityTime.Any(t => t > 0)) + { + locationElement.Add(new XAttribute("proximitytime", string.Join(',', ProximityTime.Select(i => i.ToString())))); + } LevelData.Save(locationElement); if (TypeChangeTimer > 0) @@ -751,6 +750,40 @@ namespace Barotrauma return locationElement; } + public int Distance(Location other, int maxRecursionDepth, int currRecursionDepth = 0) + { + if (currRecursionDepth >= maxRecursionDepth) { return -1; } + if (other == this) { return 0; } + int minDist = -1; + foreach (Location connected in Connections.Select(c => c.Locations.First(l => l != this))) + { + int dist = connected.Distance(other, maxRecursionDepth, currRecursionDepth+1); + if (dist >= 0) + { + if (minDist < 0 || dist < minDist) { minDist = dist; } + } + } + return minDist; + } + + public void DetermineProximityTime(Location currentLocation) + { + int dist = Distance(currentLocation, Type.CanChangeTo.Select(cct => cct.RequiredProximityForProbabilityIncrease).Max()); + for (int i=0;i 5) { ProximityTime[i] = 5; } + } + else + { + ProximityTime[i]--; + if (ProximityTime[i] < 0) { ProximityTime[i] = 0; } + } + } + } + public void Remove() { RemoveProjSpecific(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/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..d5ad202d0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationTypeChange.cs @@ -7,9 +7,13 @@ 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 List Messages = new List(); //the change can't happen if there's a location of the given type next to this one @@ -24,6 +28,9 @@ namespace Barotrauma Probability = element.GetAttributeFloat("probability", 1.0f); RequiredDuration = element.GetAttributeInt("requiredduration", 0); + ProximityProbabilityIncrease = element.GetAttributeFloat("proximityprobabilityincrease", 0.0f); + RequiredProximityForProbabilityIncrease = element.GetAttributeInt("requiredproximityforprobabilityincrease", 0); + DisallowedAdjacentLocations = element.GetAttributeStringArray("disallowedadjacentlocations", new string[0]).ToList(); RequiredAdjacentLocations = element.GetAttributeStringArray("requiredadjacentlocations", new string[0]).ToList(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index f5c8ad934..ab81bff7b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -101,7 +101,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 +357,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) { @@ -682,10 +684,12 @@ 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]; //check if there are any adjacent locations that would prevent the change bool disallowedFound = false; foreach (string disallowedLocationName in typeChange.DisallowedAdjacentLocations) @@ -714,15 +718,19 @@ namespace Barotrauma if (location.TypeChangeTimer >= typeChange.RequiredDuration) { - readyTypeChanges.Add(typeChange); + readyTypeChanges.Add(i); } } //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 => cct[i].Probability + (cct[i].ProximityProbabilityIncrease * (float)location.ProximityTime[i]))) { - var selectedTypeChange = - ToolBox.SelectWeightedRandom(readyTypeChanges, readyTypeChanges.Select(t => t.Probability).ToList(), Rand.RandSync.Unsynced); + var selectedTypeChangeIndex = + ToolBox.SelectWeightedRandom( + readyTypeChanges, + readyTypeChanges.Select(i => cct[i].Probability + (cct[i].ProximityProbabilityIncrease * (float)location.ProximityTime[i])).ToList(), + Rand.RandSync.Unsynced); + var selectedTypeChange = cct[selectedTypeChangeIndex]; if (selectedTypeChange != null) { string prevName = location.Name; 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..95bb13bef 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 @@ -343,8 +356,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) + : base(sp, submarine, id) { System.Diagnostics.Debug.Assert(rectangle.Width > 0 && rectangle.Height > 0); if (rectangle.Width == 0 || rectangle.Height == 0) { return; } @@ -399,7 +412,7 @@ namespace Barotrauma else { Sections = new WallSection[1]; - Sections[0] = new WallSection(rect); + Sections[0] = new WallSection(rect, this); if (StairDirection != Direction.None) { @@ -552,7 +565,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 +573,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 +796,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 +911,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,15 +934,13 @@ 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); } @@ -1259,7 +1270,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,12 +1283,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)) { Submarine = submarine, - ID = (ushort)int.Parse(element.Attribute("ID").Value) }; - s.OriginalID = s.ID; SerializableProperty.DeserializeProperties(s, element); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index 97036cc5f..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; } @@ -1565,14 +1676,22 @@ namespace Barotrauma if (connectedWp.isObstructed) { continue; } Vector2 start = ConvertUnits.ToSimUnits(wp.WorldPosition) - otherSub.SimPosition; Vector2 end = ConvertUnits.ToSimUnits(connectedWp.WorldPosition) - otherSub.SimPosition; - var body = Submarine.PickBody(start, end, null, Physics.CollisionWall, allowInsideFixture: false); - if (body != null && body.UserData is Structure && !((Structure)body.UserData).IsPlatform) + var body = PickBody(start, end, null, Physics.CollisionWall, allowInsideFixture: true); + if (body != null) { - connectedWp.isObstructed = true; - wp.isObstructed = true; - obstructedNodes.Add(node); - obstructedNodes.Add(connection); - break; + if (body.UserData is Structure wall && !wall.IsPlatform || body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) + { + connectedWp.isObstructed = true; + wp.isObstructed = true; + if (!obstructedNodes.TryGetValue(otherSub, out HashSet nodes)) + { + nodes = new HashSet(); + obstructedNodes.Add(otherSub, nodes); + } + nodes.Add(node); + nodes.Add(connection); + break; + } } } } @@ -1581,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..dfbbb22fd 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; } #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); + 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 && 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, + damageLevelWalls: false); +#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++) @@ -843,27 +850,7 @@ namespace Barotrauma applyDamage ? impact * ImpactDamageMultiplier : 0.0f); #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..8b76495b3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs @@ -105,8 +105,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 +626,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 +635,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 +659,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..f254b4a65 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs @@ -123,6 +123,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 +166,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 +201,7 @@ namespace Barotrauma } public EntitySpawner() - : base(null) + : base(null, Entity.EntitySpawnerID) { spawnQueue = new Queue(); removeQueue = new Queue(); @@ -253,7 +291,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..ee39ccd9d 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 { @@ -46,7 +47,7 @@ namespace Barotrauma.Networking //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 + ")"); + DebugConsole.ThrowError("Too much data in network event for entity \"" + e.Entity.ToString() + "\" (" + tempEventBuffer.LengthBytes + " bytes, event ID " + e.ID + $", {string.Join(' ',e.Data.Select(d => d.ToString()))})"); GameAnalyticsManager.AddErrorEventOnce("NetEntityEventManager.Write:TooLong" + e.Entity.ToString(), GameAnalyticsSDK.Net.EGAErrorSeverity.Error, "Too much data in network event for entity \"" + e.Entity.ToString() + "\" (" + tempEventBuffer.LengthBytes + " bytes, event ID " + e.ID + ")"); 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/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/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..6e51f1c45 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 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..6c6d6e988 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,8 @@ namespace Voronoi2 public Vector2 Translation; + public bool Island; + public Vector2 Center { get { return new Vector2((float)Site.Coord.X, (float)Site.Coord.Y) + Translation; } @@ -204,8 +206,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..1d5dbda99 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) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs index 67069c0ee..b2308c19a 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,11 +143,11 @@ 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: + 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..91e2085c9 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; @@ -187,6 +210,8 @@ namespace Barotrauma public readonly float SeverLimbsProbability; + public PhysicsBody sourceBody; + public HashSet TargetIdentifiers { get { return targetIdentifiers; } @@ -268,6 +293,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 +320,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); @@ -334,10 +362,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 +418,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); @@ -521,7 +569,7 @@ namespace Barotrauma if (item.Removed || !IsValidTarget(item)) { continue; } if (CheckDistance(item)) { - targets.Add(item); + targets.AddRange(item.AllPropertyObjects); } } } @@ -688,7 +736,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) { @@ -790,6 +848,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) { @@ -936,8 +1015,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/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..d1cc6da73 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) @@ -658,7 +723,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 3d3b56903..065f88283 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 91504bf7f..613bd67bd 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 899b0e13f..713442c9e 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 1725c25c4..5b6e5d0a8 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 30744ae23..e3317c35c 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 ce8ce2c69..90bcc7fb5 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 3d50aca0a..279fb80bc 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 554cac19c..2bf89aee5 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 56a0ffaad..0d0b0d76a 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Remora.sub and b/Barotrauma/BarotraumaShared/Submarines/Remora.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Selkie.sub b/Barotrauma/BarotraumaShared/Submarines/Selkie.sub index 124bd73b2..4840c9d73 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Selkie.sub and b/Barotrauma/BarotraumaShared/Submarines/Selkie.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Typhon.sub b/Barotrauma/BarotraumaShared/Submarines/Typhon.sub index 24115cf94..9a70b2bb9 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 c0e2cfd04..90919f251 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub and b/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Venture.sub b/Barotrauma/BarotraumaShared/Submarines/Venture.sub index 982a2248e..97d16a3a3 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Venture.sub and b/Barotrauma/BarotraumaShared/Submarines/Venture.sub differ diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 4172ff77a..c125aa4bd 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,11 +1,215 @@ --------------------------------------------------------------------------------------------------------- -v0.10.6.0 (Unstable) +v0.1100.0.4 (unstable) +--------------------------------------------------------------------------------------------------------- + +- Improvements to spineling. +- More improvements to level art and layouts. +- Added new military outpost music track. +- Added mineral scanning functionality for the sonars of Berilia and R-29. +- Devices in beacon stations no longer deteriorate by themselves. +- Added an airlock on the top of the beacon stations. +- Deep Diver subs can dive 20% deeper than other subs without getting crushed. +- Added mineral mining missions. + +Bugfixes: +- Fixed destroyed walls not disappearing client-side. +- Fixed linked submarines not loading. +- Fixed occasional "missing entity" errors caused by the server failing to write an ID card's data in a network message. +- Fixed 'ID taken by Galldren' error when placing ItemAssemblies in submarine editor. +- Fixed a bug that sometimes caused power to desync in multiplayer: when connecting the second end of a wire to a device other than a junction box, the server would sometimes not register the wire as being connected. +- The player initiating a ready check no longer has to answer the the check. +- Fixed a crash caused by the ready check. +- Fixed hull upgrades not affecting the sub's crush depth. +- Fixed console errors when spineling's spike gets stuck to a door. + +--------------------------------------------------------------------------------------------------------- +v0.1100.0.3 (unstable) +--------------------------------------------------------------------------------------------------------- + +- Added beacon missions where you have to repair and power up a "beacon station". +- Misc level generation improvements. +- Improvements to level art. +- Handheld sonar can be used to scan for minerals. +- More descriptive bot dialog when they can't find items they're looking for. +- Improved the way ballast flora drains power from the sub: instead of a static load, the load fluctuates (and causes more problems with the grid!). +- Made destructible ice walls (including spires) easier to destroy. +- Made ballast flora spores less common and removed them altogether from Cold Caverns. +- Made autopilot better at avoiding ice spires. +- Bots keep more distance to the player while following underwater and outside the sub. +- Reduced scrap spawn rates in wrecks. +- Removed the explosive cargo mission variant where one of the explosives spontaneously explodes. +- Improved the way destructible cells break into smaller fragments (ice spires in particular). +- Split combat missions into a separate game mode. +- Added mineral scanning functionality for sonars: enabled by default for handheld sonars, can be enabled for other sonar devices in the Sub Editor with Sonar component's "HasMineralScanner" property + +Bugfixes: +- Fixed frequent "level mismatch" errors in multiplayer due to mismatching resource spawns. +- Fixed wires not appearing in some item assemblies placed in the sub editor until the sub is saved. +- Fixed decorative sprites being positioned incorrectly on mirrored items (such as pumps and fabricators). +- Fixed sonar somatimes calculating the distance to targets incorrectly. +- Fixed caves sometimes spawning in open water. +- Fixed level floor not being rendered fully when zoomed in. +- Fixed nest's sonar label being positioned incorrectly client-side. + +--------------------------------------------------------------------------------------------------------- +v0.1100.0.2 (unstable) +--------------------------------------------------------------------------------------------------------- + +Overhauled environments (WIP): +- 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. + +Additions and changes: +- Added nest missions where you need to enter a cave to destroy a monster nest. +- Submarine hull upgrades increase the submarine's tolerance to pressure, allowing it to dive deeper without getting crushed by pressure. +- Added damage particles when dealing damage to ballast flora. +- Added lights to spineling's spikes. +- Made thermal artifacts a bit more manageable: they now start a fires periodically, not continuously. +- Made ballast flora more vulnerable to fire. +- Damaging ballast flora increases karma. + +Bugfixes: +- Fixed projectiles sometimes phasing through the spineling without damaging it. +- Fixed spineling's spike projectiles behaving erratically in multiplayer. +- Fixed in ability to damage huskified crew members if friendly fire is disabled. +- Fixed prototype steam cannon particles going through walls. +- Fixed "engineers are special" outpost event not giving XP when successfully helping the NPC. +- Fixed "propaganda" and "clown outbreak" outpost events not triggering. + +--------------------------------------------------------------------------------------------------------- +v0.1100.0.1 (unstable) +--------------------------------------------------------------------------------------------------------- + +- Added a new monster, "Spineling". +- Fixed oxygen generators. +- Added concatenation component (a signal component that joins two inputs together). +- Added toxin attack to ballast flora. +- Fixed flamer doing more damage to the ballast flora than it should. +- Fixed projectiles being blocked by ballast flora. +- Fixed ballast flora sometimes closing doors permanently. +- Made welding tools, plasma cutters and explosives hurt ballast flora. +- Fixed console errors when setting an engine's max force to 0. +- Fixed crashing when a disguised human turns into a husk. +- Fixed disconnected, hanging wires sometimes appearing at the wrong end of the wire. +- Fixed progress bar saying "welding" instead of "cutting" when cutting open a welded door. +- Fixed explosion damage to items not being diminished if there are obstacles between the explosion and the item. +- Fixes to flamer particles going through walls. +- Fixed items that are included in multiple categories (e.g. oxygen tanks, battery cells) not appearing in the sub editor's entity list unless using the search bar. +- Fixed psychosis sounds affecting all players. +- Fixed fabricators and deconstructors deteriorating even if they're not running. +- Fixed crashing when selecting a fabricator linked to a deconstructor or vice versa. +- Fixed "hide offensive server names" tickbox working the wrong way around in the server browser. + +--------------------------------------------------------------------------------------------------------- +v0.1100.0.0 (unstable) +--------------------------------------------------------------------------------------------------------- + +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 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. +- Diving suits the bots have dropped in an outpost are automatically moved to the sub when departing from the outpost. +- Outpost security allows "stealing" diving masks and suits if the outpost is flooding. +- Added water percentage output to water detector. + +Bugfixes: +- Fixed previous messages disappearing from terminals and logbooks when transitioning to a new level in the campaign. +- Fixed sprite depths in dockingmodule 2, should prevent z-fighting. +- Fixed bots getting stuck while swimming near the submarine, because they kepth switching between different steering modes. +- Fixed bots not avoiding other submarines connected to the submarine they are heading to while swimming around the submarine using waypoints. +- Fixed 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. +- Fixed Health Scanner HUD showing a disguised character's true identity. +- Fixed health interface showing the original face and occupation of disguised characters. +- Fixed changelog layout getting messed up in the main menu after changing the resolution. +- Fixed some structures turning into wrecked versions when reloading the core content package with the "reloadcorepackage" console command. +- Fixed vent output being calculated incorrectly in multi-hull rooms. +- Fixed broken walls near hatches/doors sometimes preventing characters from entering the sub/outpost through the hatch/door. +- Fixed switch state being toggled when selecting them in the sub editor's wiring mode. +- Fixed engine not being affected by low power unless the voltage is low enough to turn it off completely (in practice meaning that there's not benefit to supplying the engine more than 50% of it's power consumption). +- Fixed characters getting healed between campaign rounds in single player. +- 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 crashing when a bot abandons AIObjectiveCombat due to the target being in a different sub (e.g. if a character the outpost security is chasing moves from the outpost to the sub). +- Fixed pet's hunger/happiness values and inventories not getting saved between rounds. +- Fixed pet name tags disappearing client-side between rounds. +- Fixed empty oxygen tank not triggering the warning sound of a diving suit's oxygen supply. +- 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. + +--------------------------------------------------------------------------------------------------------- +v0.10.6.2 +--------------------------------------------------------------------------------------------------------- + +- Adjusted pets' item production rates and hunger/happiness thresholds. +- Fixed inability to pick up chitin chunks. +- Fixed event manager considering pets to be enemies, leading to monster spawns being delayed or disabled altogether when there are pets inside the sub. +- Fixed bots having difficulties in entering/exiting the airlocks. +- Fixed doors not obstructing waypoints after docking. +- Fixes to pet syncing. +- Fixes to pets disappearing when transitioning between levels. +- Removed small crawler egg (was only intended for testing). +- Kastrull: Fixed the airlock waypoints not being linked to the doors, causing the bots not being able to operate them. + +--------------------------------------------------------------------------------------------------------- +v0.10.6.1 +--------------------------------------------------------------------------------------------------------- + +- Added sounds for pets. +- Most fruits can be fed to pets. +- Increased the time it takes for pets to get hungry. +- Decreased pets' item production rate. +- The number of monsters now increases the event intensity more than previously, which should spawn monsters less frequently. +- Added some unarmored mudraptors to low-difficulty levels. +- Fixed crashing when a character takes damage from something else than another character attacking them (e.g. volcanoes). +- Fixed some steering issues where bots would return to the last waypoint instead of continuing with their current path when they should. +- Fixed occasional "velocity invalid" error messages when a character gets hit by a very fast projectile (e.g. coilgun bolt). +- Fixed severed tiger thresher heads doing the "death wiggle" animation. +- Fixed water flow sounds not disappearing over time. +- Fixed throwing an item that has no status effects (such as a flare) causing a crash. +- Fixed chitin helmet spawning in armory cabinets. +- Fixed event intensity going down immediately when the situation gets less intense instead of gradually returning to normal. Should make it happen less that more monsters are spawned soon after the player survives the previous wave of monsters. +- Fixed monsters using the Escape state when they should use the Flee state. +- Fixed Threshers not dying after being beheaded. +- Fixed heads or other extruding limbs being severed also when their root body takes damage. +- Fixed bots trying to get inside outposts (from outside) that they can't and shouldn't be able to enter to. +- Fixed very small creatures getting stuck on waypoints. +- Test changing the assignment logic for maintenance/operate orders: if the player doesn't specify the target character, use the bot who already is following the same order. The intention is to make it easier to change the target item of the order. The draw back is that ordering multiple bots to man the turrets now requires an extra step: specify the target character. + +--------------------------------------------------------------------------------------------------------- +v0.10.6.0 --------------------------------------------------------------------------------------------------------- Changes and additions: - Reworked Watcher. - Added pets (can be obtained by buying eggs from outposts). The pets produce items that can be used for crafting if they're kept happy and well-fed. -- Added a new monster behavior: observe. +- Added a new AI behaviors: Observe, Follow and Freeze. - Added toolbelt (a wearable container with a capacity of 12 and it's own dedicated slot) as a replacement for the toolbox. - Improvements to the effects caused by psychosis: the affliction icon is not visible to the psychotic character, the fake fires and floods are a bit more convincing, the affliction plays random sounds and can cause other characters to become invisible. - Added capture group support to RegEx component. @@ -33,7 +237,6 @@ Changes and additions: - Added sounds for the nausea affliction. - Nausea now inflicts a minor stun and internal damage when the character throws up. - Monsters now stop fleeing after a while, if they are not being chased and can't perceive the target anymore. -- A minor change to the status effect condition targeting logic: If "This" and "NearbyCharacters" are both defined as the targets of a status effect, the conditions only apply to "this" entity, even though the effects are applied on all the targets. - Implement spread, speed, and rotation for the spawn item status effects. - Minor damage (less than 1 hp) doesn't spawn particles anymore. - Rebalanced upgrade parameters, allowing for more noticable benefits. @@ -43,10 +246,20 @@ Changes and additions: - Made large monsters immune to paralyzant (mudraptor is the largest affected monster). - Use player name instead of server name for the server owner when hosting a server. - Don't draw turret range indicators in the sub editor when the turret isn't selected. +- Adjusted Hammerhead's posture. +- Minor adjustments to Mudraptor's animations. + +Modding: +- Added Scale and Offset to Light Sprite's parameters. +- Fixed the Constant Torque parameter not working right. +- Allow to enable/disable tail angles per limb. Previously the angle was only applied to the first limb of type Tail. +- Added per limb multipliers for sine animations (fish tail movement). +- Exposed the fleeing and avoiding times on the monster AI parameters. +- The Light Sources on characters can now be defined with conditionals. +- Added HealthMultiplier parameter that can be used in StatusEffects like SpeedMultiplier. Character Editor: - Fixed a number of issues with the joint limit widgets. Also allowed to set a joint to rotate clockwise, which inverses the widget direction. Useful for heads or other limbs that extrude right from the main body. -- Inversed the default joint ends, because it's more usual case to edit the second limb of the joint than the first. - The colliders of the hidden limbs are now hidden in the game view. - Changed the hotkey for toggling the parameter editor from "Tab" to "F1" and fix the inability to toggle the editor when a text field is selected. - Fixed load and save interfaces being broken on lower resolutions. diff --git a/Libraries/Facepunch.Steamworks/SteamNetworkingUtils.cs b/Libraries/Facepunch.Steamworks/SteamNetworkingUtils.cs index 3cf44fb41..e78c07bf0 100644 --- a/Libraries/Facepunch.Steamworks/SteamNetworkingUtils.cs +++ b/Libraries/Facepunch.Steamworks/SteamNetworkingUtils.cs @@ -123,6 +123,7 @@ namespace Steamworks /// public static async Task WaitForPingDataAsync( float maxAgeInSeconds = 60 * 5 ) { + await Task.Yield(); if ( Internal.CheckPingDataUpToDate( maxAgeInSeconds ) ) return; 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;