diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs index e01494501..74548b53d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs @@ -81,10 +81,10 @@ namespace Barotrauma ConvertUnits.ToDisplayUnits(new Vector2(attachJoint.WorldAnchorB.X, -attachJoint.WorldAnchorB.Y)), GUI.Style.Green, 0, 4); } - if (LatchOntoAI.WallAttachPos.HasValue) + if (LatchOntoAI.AttachPos.HasValue) { - //GUI.DrawLine(spriteBatch, pos, - // ConvertUnits.ToDisplayUnits(new Vector2(LatchOntoAI.WallAttachPos.Value.X, -LatchOntoAI.WallAttachPos.Value.Y)), GUI.Style.Green, 0, 3); + GUI.DrawLine(spriteBatch, pos, + ConvertUnits.ToDisplayUnits(new Vector2(LatchOntoAI.AttachPos.Value.X, -LatchOntoAI.AttachPos.Value.Y)), GUI.Style.Green, 0, 3); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs index 627c70255..de34b1006 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs @@ -562,8 +562,10 @@ namespace Barotrauma if (this is HumanoidAnimController humanoid) { Vector2 pos = ConvertUnits.ToDisplayUnits(humanoid.RightHandIKPos); + if (humanoid.character.Submarine != null) { pos += humanoid.character.Submarine.Position; } GUI.DrawRectangle(spriteBatch, new Rectangle((int)pos.X, (int)-pos.Y, 4, 4), GUI.Style.Green, true); pos = ConvertUnits.ToDisplayUnits(humanoid.LeftHandIKPos); + if (humanoid.character.Submarine != null) { pos += humanoid.character.Submarine.Position; } GUI.DrawRectangle(spriteBatch, new Rectangle((int)pos.X, (int)-pos.Y, 4, 4), GUI.Style.Green, true); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index 89859485a..3c8d3d5a0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -100,7 +100,7 @@ namespace Barotrauma } } - private static bool shouldRecreateHudTexts = true; + public static bool ShouldRecreateHudTexts { get; set; } = true; private static bool heldDownShiftWhenGotHudTexts; public static bool IsCampaignInterfaceOpen => @@ -150,7 +150,7 @@ namespace Barotrauma } } - if (character.IsHumanoid && character.SelectedCharacter != null) + if (character.Params.CanInteract && character.SelectedCharacter != null) { character.SelectedCharacter.CharacterHealth.AddToGUIUpdateList(); } @@ -195,7 +195,7 @@ namespace Barotrauma } } - if (character.IsHumanoid && character.SelectedCharacter != null && character.SelectedCharacter.Inventory != null) + if (character.Params.CanInteract && character.SelectedCharacter != null && character.SelectedCharacter.Inventory != null) { if (character.SelectedCharacter.CanInventoryBeAccessed) { @@ -219,7 +219,7 @@ namespace Barotrauma if (focusedItemOverlayTimer <= 0.0f) { focusedItem = null; - shouldRecreateHudTexts = true; + ShouldRecreateHudTexts = true; } } } @@ -285,6 +285,21 @@ namespace Barotrauma i.GetRootInventoryOwner() == i); } + if (GameMain.GameSession != null) + { + foreach (var mission in GameMain.GameSession.Missions) + { + if (!mission.DisplayTargetHudIcons) { continue; } + foreach (var target in mission.HudIconTargets) + { + if (target.Submarine != character.Submarine) { continue; } + float alpha = GetDistanceBasedIconAlpha(target, maxDistance: mission.Prefab.HudIconMaxDistance); + if (alpha <= 0.0f) { continue; } + GUI.DrawIndicator(spriteBatch, target.DrawPosition, cam, 100.0f, mission.Prefab.HudIcon, mission.Prefab.HudIconColor * alpha); + } + } + } + foreach (Character.ObjectiveEntity objectiveEntity in character.ActiveObjectiveEntities) { DrawObjectiveIndicator(spriteBatch, cam, character, objectiveEntity, 1.0f); @@ -317,7 +332,7 @@ namespace Barotrauma if (focusedItem != character.FocusedItem) { focusedItemOverlayTimer = Math.Min(1.0f, focusedItemOverlayTimer); - shouldRecreateHudTexts = true; + ShouldRecreateHudTexts = true; } focusedItem = character.FocusedItem; } @@ -342,13 +357,13 @@ namespace Barotrauma if (!GUI.DisableItemHighlights && !Inventory.DraggingItemToWorld) { bool shiftDown = PlayerInput.IsShiftDown(); - if (shouldRecreateHudTexts || heldDownShiftWhenGotHudTexts != shiftDown) + if (ShouldRecreateHudTexts || heldDownShiftWhenGotHudTexts != shiftDown) { - shouldRecreateHudTexts = true; + ShouldRecreateHudTexts = true; heldDownShiftWhenGotHudTexts = shiftDown; } - var hudTexts = focusedItem.GetHUDTexts(character, shouldRecreateHudTexts); - shouldRecreateHudTexts = false; + var hudTexts = focusedItem.GetHUDTexts(character, ShouldRecreateHudTexts); + ShouldRecreateHudTexts = false; int dir = Math.Sign(focusedItem.WorldPosition.X - character.WorldPosition.X); @@ -490,7 +505,7 @@ namespace Barotrauma if (!character.IsIncapacitated && character.Stun <= 0.0f) { - if (character.IsHumanoid && character.SelectedCharacter != null && character.SelectedCharacter.Inventory != null) + if (character.Params.CanInteract && character.SelectedCharacter != null && character.SelectedCharacter.Inventory != null) { if (character.SelectedCharacter.CanInventoryBeAccessed) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index 2ab8174b8..2027b879d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -17,11 +17,15 @@ namespace Barotrauma public bool LastControlled; + #warning TODO: Refactor private Sprite disguisedPortrait; private List disguisedAttachmentSprites; private Vector2? disguisedSheetIndex; private Sprite disguisedJobIcon; private Color disguisedJobColor; + private Color disguisedHairColor; + private Color disguisedFacialHairColor; + private Color disguisedSkinColor; private Sprite tintMask; private float tintHighlightThreshold; @@ -161,10 +165,10 @@ namespace Barotrauma private void DrawInfoFrameCharacterIcon(SpriteBatch sb, Rectangle componentRect) { - if (headSprite == null) { return; } + if (_headSprite == null) { return; } Vector2 targetAreaSize = componentRect.Size.ToVector2(); - float scale = Math.Min(targetAreaSize.X / headSprite.size.X, targetAreaSize.Y / headSprite.size.Y); - DrawIcon(sb, componentRect.Location.ToVector2() + headSprite.size / 2 * scale, targetAreaSize); + float scale = Math.Min(targetAreaSize.X / _headSprite.size.X, targetAreaSize.Y / _headSprite.size.Y); + DrawIcon(sb, componentRect.Location.ToVector2() + _headSprite.size / 2 * scale, targetAreaSize); } public GUIFrame CreateCharacterFrame(GUIComponent parent, string text, object userData) @@ -227,193 +231,36 @@ namespace Barotrauma { if (idCard.Item.Tags == string.Empty) return; - if (idCard.StoredJobPrefab == null || idCard.StoredPortrait == null) + if (idCard.StoredOwnerAppearance.JobPrefab == null || idCard.StoredOwnerAppearance.Portrait == null) { string[] readTags = idCard.Item.Tags.Split(','); - if (readTags.Length == 0) return; + if (readTags.Length == 0) { return; } - if (idCard.StoredJobPrefab == null) + if (idCard.StoredOwnerAppearance.JobPrefab == null) { - string jobIdTag = readTags.FirstOrDefault(s => s.StartsWith("jobid:")); - - if (jobIdTag != null && jobIdTag.Length > 6) - { - string jobId = jobIdTag.Substring(6); - if (jobId != string.Empty) - { - idCard.StoredJobPrefab = JobPrefab.Get(jobId); - } - } + idCard.StoredOwnerAppearance.ExtractJobPrefab(readTags); } - if (idCard.StoredPortrait == null) + if (idCard.StoredOwnerAppearance.Portrait == 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))); - } - } + idCard.StoredOwnerAppearance.ExtractAppearance(this, readTags); } } - if (idCard.StoredJobPrefab != null) + if (idCard.StoredOwnerAppearance.JobPrefab != null) { - disguisedJobIcon = idCard.StoredJobPrefab.Icon; - disguisedJobColor = idCard.StoredJobPrefab.UIColor; + disguisedJobIcon = idCard.StoredOwnerAppearance.JobPrefab.Icon; + disguisedJobColor = idCard.StoredOwnerAppearance.JobPrefab.UIColor; } - disguisedPortrait = idCard.StoredPortrait; - disguisedSheetIndex = idCard.StoredSheetIndex; - disguisedAttachmentSprites = idCard.StoredAttachments; + disguisedPortrait = idCard.StoredOwnerAppearance.Portrait; + disguisedSheetIndex = idCard.StoredOwnerAppearance.SheetIndex; + disguisedAttachmentSprites = idCard.StoredOwnerAppearance.Attachments; + + disguisedHairColor = idCard.StoredOwnerAppearance.HairColor; + disguisedFacialHairColor = idCard.StoredOwnerAppearance.FacialHairColor; + disguisedSkinColor = idCard.StoredOwnerAppearance.SkinColor; } partial void LoadAttachmentSprites(bool omitJob) @@ -462,24 +309,35 @@ namespace Barotrauma public void DrawPortrait(SpriteBatch spriteBatch, Vector2 screenPos, Vector2 offset, float targetWidth, bool flip = false, bool evaluateDisguise = false) { - if (evaluateDisguise && IsDisguised) return; + if (evaluateDisguise && IsDisguised) { return; } Vector2? sheetIndex; Sprite portraitToDraw; List attachmentsToDraw; + Color hairColor; + Color facialHairColor; + Color skinColor; + if (!IsDisguisedAsAnother || !evaluateDisguise) { sheetIndex = Head.SheetIndex; portraitToDraw = Portrait; attachmentsToDraw = AttachmentSprites; + + hairColor = Head.HairColor; + facialHairColor = Head.FacialHairColor; + skinColor = Head.SkinColor; } else { - //TODO: disguise skin and hair colors sheetIndex = disguisedSheetIndex; portraitToDraw = disguisedPortrait; attachmentsToDraw = disguisedAttachmentSprites; + + hairColor = disguisedHairColor; + facialHairColor = disguisedFacialHairColor; + skinColor = disguisedSkinColor; } if (portraitToDraw != null) @@ -492,14 +350,14 @@ namespace Barotrauma SetHeadEffect(spriteBatch); portraitToDraw.SourceRect = new Rectangle(CalculateOffset(portraitToDraw, sheetIndex.Value.ToPoint()), portraitToDraw.SourceRect.Size); } - portraitToDraw.Draw(spriteBatch, screenPos + offset, SkinColor, portraitToDraw.Origin, scale: scale, spriteEffect: flip ? SpriteEffects.FlipHorizontally : SpriteEffects.None); + portraitToDraw.Draw(spriteBatch, screenPos + offset, skinColor, portraitToDraw.Origin, scale: scale, spriteEffect: flip ? SpriteEffects.FlipHorizontally : SpriteEffects.None); if (attachmentsToDraw != null) { float depthStep = 0.000001f; foreach (var attachment in attachmentsToDraw) { SetAttachmentEffect(spriteBatch, attachment); - DrawAttachmentSprite(spriteBatch, attachment, portraitToDraw, sheetIndex, screenPos + offset, scale, depthStep, GetAttachmentColor(attachment), flip ? SpriteEffects.FlipHorizontally : SpriteEffects.None); + DrawAttachmentSprite(spriteBatch, attachment, portraitToDraw, sheetIndex, screenPos + offset, scale, depthStep, GetAttachmentColor(attachment, hairColor, facialHairColor), flip ? SpriteEffects.FlipHorizontally : SpriteEffects.None); depthStep += depthStep; } } @@ -516,7 +374,7 @@ namespace Barotrauma { headEffectParameters.Effect ??= GameMain.GameScreen.ThresholdTintEffect; headEffectParameters.Params ??= new Dictionary(); - headEffectParameters.Params["xBaseTexture"] = headSprite.Texture; + headEffectParameters.Params["xBaseTexture"] = HeadSprite.Texture; headEffectParameters.Params["xTintMaskTexture"] = tintMask?.Texture ?? GUI.WhiteTexture; headEffectParameters.Params["xCutoffTexture"] = GUI.WhiteTexture; headEffectParameters.Params["baseToCutoffSizeRatio"] = 1.0f; @@ -541,15 +399,15 @@ namespace Barotrauma spriteBatch.SwapEffect(attachmentEffectParameters[attachment.Type]); } - private Color GetAttachmentColor(WearableSprite attachment) + private Color GetAttachmentColor(WearableSprite attachment, Color hairColor, Color facialHairColor) { switch (attachment.Type) { case WearableType.Hair: - return HairColor; + return hairColor; case WearableType.Beard: case WearableType.Moustache: - return FacialHairColor; + return facialHairColor; default: return Color.White; } @@ -574,7 +432,7 @@ namespace Barotrauma foreach (var attachment in AttachmentSprites) { SetAttachmentEffect(spriteBatch, attachment); - DrawAttachmentSprite(spriteBatch, attachment, headSprite, Head.SheetIndex, screenPos, scale, depthStep, GetAttachmentColor(attachment)); + DrawAttachmentSprite(spriteBatch, attachment, headSprite, Head.SheetIndex, screenPos, scale, depthStep, GetAttachmentColor(attachment, HairColor, FacialHairColor)); depthStep += depthStep; } } @@ -718,6 +576,7 @@ namespace Barotrauma private readonly GUIComponent parentComponent; private readonly List characterSprites = new List(); + public GUIButton RandomizeButton; public AppearanceCustomizationMenu(CharacterInfo info, GUIComponent parent, bool hasIcon = true) { @@ -737,13 +596,12 @@ namespace Barotrauma ClearSprites(); float contentWidth = HasIcon ? 0.75f : 1.0f; - var content = - new GUIListBox( - new RectTransform(new Vector2(contentWidth, 1.0f), parentComponent.RectTransform, - Anchor.CenterLeft)) - { CanBeFocused = false, CanTakeKeyBoardFocus = false } - .Content; - + var listBox = new GUIListBox( + new RectTransform(new Vector2(contentWidth, 1.0f), parentComponent.RectTransform, + Anchor.CenterLeft)) + { CanBeFocused = false, CanTakeKeyBoardFocus = false }; + var content = listBox.Content; + info.LoadHeadAttachments(); if (HasIcon) { @@ -754,7 +612,7 @@ namespace Barotrauma RectTransform createItemRectTransform(string labelTag, float width = 0.6f) { - var layoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.2f), content.RectTransform)); + var layoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.166f), content.RectTransform)); var label = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), layoutGroup.RectTransform), TextManager.Get(labelTag), font: GUI.SubHeadingFont); @@ -793,6 +651,7 @@ namespace Barotrauma info.FilterElementsByGenderAndRace(info.Wearables, info.Head.gender, info.Head.race), wearableType, info.HeadSpriteId).Count(); + List attachmentSliders = new List(); void createAttachmentSlider(int initialValue, WearableType wearableType) { int attachmentCount = countAttachmentsOfType(wearableType); @@ -812,6 +671,7 @@ namespace Barotrauma BarSize = 1.0f / (float)(attachmentCount + 1) }; slider.BarScrollValue = initialValue; + attachmentSliders.Add(slider); } } @@ -869,6 +729,36 @@ namespace Barotrauma CanBeFocused = false }; } + + var childToSelect = dropdown.ListBox.Content.FindChild(c => (Color)c.UserData == getter()); + dropdown.Select(dropdown.ListBox.Content.GetChildIndex(childToSelect)); + + //The following exists to track mouseover to preview colors before selecting them + bool previewingColor = false; + new GUICustomComponent(new RectTransform(Vector2.One, buttonFrame.RectTransform), + onUpdate: (deltaTime, component) => + { + if (GUI.MouseOn is GUIFrame { Parent: { } p } hoveredFrame && dropdown.ListBox.Content.IsParentOf(hoveredFrame)) + { + previewingColor = true; + Color color = (Color)(dropdown.ListBox.Content.FindChild(c => + c == hoveredFrame || c.IsParentOf(hoveredFrame))?.UserData ?? dropdown.SelectedData); + setter(color); + buttonFrame.Color = getter(); + buttonFrame.HoverColor = getter(); + } + else if (previewingColor) + { + setter((Color)dropdown.SelectedData); + buttonFrame.Color = getter(); + buttonFrame.HoverColor = getter(); + previewingColor = false; + } + }, onDraw: null) + { + CanBeFocused = false, + Visible = true + }; } if (countAttachmentsOfType(WearableType.Hair) > 0) @@ -886,28 +776,54 @@ namespace Barotrauma createColorSelector($"Customization.{nameof(info.SkinColor)}", info.SkinColors, () => info.SkinColor, (color) => info.SkinColor = color); + + RandomizeButton = new GUIButton(new RectTransform(Vector2.One * 0.12f, + parentComponent.RectTransform, + anchor: Anchor.BottomRight, scaleBasis: ScaleBasis.Smallest) + { RelativeOffset = new Vector2(0.01f, 0.005f) }, style: "RandomizeButton") + { + OnClicked = (button, o) => + { + var headPreset = info.Heads.Keys.GetRandom(Rand.RandSync.Unsynced); + info.Head.gender = headPreset.Gender; + info.Head.race = headPreset.Race; + info.Head.HeadSpriteId = headPreset.ID; + + info.Head.HairIndex = Rand.Int(countAttachmentsOfType(WearableType.Hair), Rand.RandSync.Unsynced); + info.Head.BeardIndex = Rand.Int(countAttachmentsOfType(WearableType.Beard), Rand.RandSync.Unsynced); + info.Head.MoustacheIndex = Rand.Int(countAttachmentsOfType(WearableType.Moustache), Rand.RandSync.Unsynced); + info.Head.FaceAttachmentIndex = Rand.Int(countAttachmentsOfType(WearableType.FaceAttachment), Rand.RandSync.Unsynced); + + info.Head.HairColor = info.HairColors.GetRandom(Rand.RandSync.Unsynced); + info.Head.FacialHairColor = info.FacialHairColors.GetRandom(Rand.RandSync.Unsynced); + info.Head.SkinColor = info.SkinColors.GetRandom(Rand.RandSync.Unsynced); + + RecreateFrameContents(); + info.RefreshHead(); + OnHeadSwitch?.Invoke(this); + attachmentSliders.ForEach(s => OnSliderMoved?.Invoke(s, s.BarScroll)); + + return false; + } + }; + //force update twice because the listbox is insanely janky + //TODO: fix all of the UI :) + listBox.ForceUpdate(); + listBox.ForceUpdate(); + foreach (var childLayoutGroup in listBox.Content.GetAllChildren()) + { + childLayoutGroup.Recalculate(); + } } private bool OpenHeadSelection(GUIButton button, object userData) { Gender selectedGender = (Gender)userData; - if (HeadSelectionList != null) - { - HeadSelectionList.Visible = true; - foreach (GUIComponent child in HeadSelectionList.Content.Children) - { - child.Visible = (Gender)child.UserData == selectedGender; - child.Children.ForEach(c => - c.Visible = ((Tuple)c.UserData).Item1 == selectedGender); - } - - return true; - } var info = CharacterInfo; float characterHeightWidthRatio = info.HeadSprite.size.Y / info.HeadSprite.size.X; - HeadSelectionList = new GUIListBox( + HeadSelectionList ??= new GUIListBox( new RectTransform( new Point(parentComponent.Rect.Width, (int)(parentComponent.Rect.Width * characterHeightWidthRatio * 0.6f)), GUI.Canvas) @@ -915,6 +831,9 @@ namespace Barotrauma AbsoluteOffset = new Point(parentComponent.Rect.Right - parentComponent.Rect.Width, button.Rect.Bottom) }); + HeadSelectionList.Visible = true; + HeadSelectionList.Content.ClearChildren(); + ClearSprites(); parentComponent.RectTransform.SizeChanged += () => { @@ -952,15 +871,14 @@ namespace Barotrauma { row = null; itemsInRow = 0; - foreach (var head in heads) + foreach (var kvp in heads.Where(kv => kv.Key.Gender == selectedGender)) { - var headPreset = head.Key; - Gender gender = headPreset.Gender; + var headPreset = kvp.Key; Race race = headPreset.Race; int headIndex = headPreset.ID; string spritePath = spritePathWithTags - .Replace("[GENDER]", gender.ToString().ToLowerInvariant()) + .Replace("[GENDER]", selectedGender.ToString().ToLowerInvariant()) .Replace("[RACE]", race.ToString().ToLowerInvariant()); if (!File.Exists(spritePath)) @@ -970,18 +888,18 @@ namespace Barotrauma Sprite headSprite = new Sprite(headSpriteElement, "", spritePath); headSprite.SourceRect = - new Rectangle(CharacterInfo.CalculateOffset(headSprite, head.Value.ToPoint()), + new Rectangle(CalculateOffset(headSprite, kvp.Value.ToPoint()), headSprite.SourceRect.Size); characterSprites.Add(headSprite); - if (itemsInRow >= 4 || row == null || gender != (Gender)row.UserData) + if (itemsInRow >= 4 || row == null) { row = new GUILayoutGroup( new RectTransform(new Vector2(1.0f, 0.333f), HeadSelectionList.Content.RectTransform), true) { - UserData = gender, - Visible = gender == selectedGender + UserData = selectedGender, + Visible = true }; itemsInRow = 0; } @@ -991,10 +909,10 @@ namespace Barotrauma { OutlineColor = Color.White * 0.5f, PressedColor = Color.White * 0.5f, - UserData = new Tuple(gender, race, headIndex), + UserData = new Tuple(selectedGender, race, headIndex), OnClicked = SwitchHead, - Selected = gender == info.Gender && race == info.Race && headIndex == info.HeadSpriteId, - Visible = gender == selectedGender + Selected = selectedGender == info.Gender && race == info.Race && headIndex == info.HeadSpriteId, + Visible = true }; new GUIImage(new RectTransform(Vector2.One, btn.RectTransform), headSprite, scaleToFit: true); @@ -1013,7 +931,7 @@ namespace Barotrauma int id = ((Tuple)obj).Item3; info.Gender = gender; info.Race = race; - info.HeadSpriteId = id; + info.Head.HeadSpriteId = id; RecreateFrameContents(); OnHeadSwitch?.Invoke(this); return true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index 78fa27ccf..e9a30ca3d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -66,8 +66,6 @@ namespace Barotrauma private SpriteSheet medUIExtra; private float medUIExtraAnimState; - private GUIComponent draggingMed; - private int highlightedLimbIndex = -1; private int selectedLimbIndex = -1; private LimbHealth currentDisplayedLimb; @@ -118,7 +116,6 @@ namespace Barotrauma if (prevOpenHealthWindow != null) { - prevOpenHealthWindow.selectedLimbIndex = -1; prevOpenHealthWindow.highlightedLimbIndex = -1; } @@ -207,7 +204,7 @@ namespace Barotrauma 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, false, openHealthWindow?.Character != Character.Controlled); + character.Info?.DrawPortrait(spriteBatch, new Vector2(component.Rect.X, component.Rect.Center.Y - component.Rect.Width / 2), Vector2.Zero, component.Rect.Width, false, character != Character.Controlled); }); characterName = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), nameContainer.RectTransform), "", textAlignment: Alignment.CenterLeft, font: GUI.SubHeadingFont) { @@ -216,7 +213,7 @@ namespace Barotrauma new GUICustomComponent(new RectTransform(new Vector2(0.2f, 1.0f), nameContainer.RectTransform), onDraw: (spriteBatch, component) => { - character.Info?.DrawJobIcon(spriteBatch, component.Rect, openHealthWindow?.Character != Character.Controlled); + character.Info?.DrawJobIcon(spriteBatch, component.Rect, character != Character.Controlled); }); @@ -275,6 +272,34 @@ namespace Barotrauma } }); + + cprButton = new GUIButton(new RectTransform(new Vector2(0.17f, 0.17f), characterIndicatorArea.RectTransform, Anchor.BottomLeft, scaleBasis: ScaleBasis.Smallest), text: "", style: "CPRButton") + { + OnClicked = (button, userData) => + { + Character selectedCharacter = Character.Controlled?.SelectedCharacter; + if (selectedCharacter == null || (!selectedCharacter.IsUnconscious && selectedCharacter.Stun <= 0.0f)) + { + return false; + } + + Character.Controlled.AnimController.Anim = (Character.Controlled.AnimController.Anim == AnimController.Animation.CPR) ? + AnimController.Animation.None : AnimController.Animation.CPR; + + selectedCharacter.AnimController.ResetPullJoints(); + + if (GameMain.Client != null) + { + GameMain.Client.CreateEntityEvent(Character.Controlled, new object[] { NetEntityEvent.Type.Treatment }); + } + + return true; + }, + ToolTip = TextManager.Get("doctor.cprobjective"), + IgnoreLayoutGroups = true, + Visible = false + }; + var limbSelection = new GUICustomComponent(new RectTransform(new Vector2(0.4f, 1.0f), characterIndicatorArea.RectTransform), (spriteBatch, component) => { @@ -300,7 +325,7 @@ namespace Barotrauma deadIndicator.AutoScaleHorizontal = true; } - afflictionIconContainer = new GUIListBox(new RectTransform(new Vector2(0.25f, 0.7f), characterIndicatorArea.RectTransform), style: null); + afflictionIconContainer = new GUIListBox(new RectTransform(new Vector2(0.25f, 1.0f), characterIndicatorArea.RectTransform), style: null); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), healthWindowVerticalLayout.RectTransform), TextManager.Get("SuitableTreatments"), font: GUI.SubHeadingFont, textAlignment: Alignment.BottomCenter); @@ -324,33 +349,6 @@ namespace Barotrauma characterIndicatorArea.Recalculate(); - cprButton = new GUIButton(new RectTransform(new Vector2(afflictionIconContainer.RectTransform.RelativeSize.X, 0.3f), characterIndicatorArea.RectTransform, Anchor.BottomRight, scaleBasis: ScaleBasis.Smallest), text: "", style: "CPRButton") - { - OnClicked = (button, userData) => - { - Character selectedCharacter = Character.Controlled?.SelectedCharacter; - if (selectedCharacter == null || (!selectedCharacter.IsUnconscious && selectedCharacter.Stun <= 0.0f)) - { - return false; - } - - Character.Controlled.AnimController.Anim = (Character.Controlled.AnimController.Anim == AnimController.Animation.CPR) ? - AnimController.Animation.None : AnimController.Animation.CPR; - - selectedCharacter.AnimController.ResetPullJoints(); - - if (GameMain.Client != null) - { - GameMain.Client.CreateEntityEvent(Character.Controlled, new object[] { NetEntityEvent.Type.Treatment }); - } - - return true; - }, - ToolTip = TextManager.Get("doctor.cprobjective"), - IgnoreLayoutGroups = true, - Visible = false - }; - healthBarHolder = new GUIFrame(new RectTransform(Point.Zero, GUI.Canvas), style: null) { HoverCursor = CursorState.Hand @@ -721,20 +719,19 @@ namespace Barotrauma foreach (GUIComponent afflictionIcon in afflictionIconContainer.Content.Children) { if (!(afflictionIcon.UserData is Affliction affliction)) { continue; } - var btn = afflictionIcon.GetChild(); - if (affliction.AppliedAsFailedTreatmentTime > Timing.TotalTime - 1.0 && btn.FlashTimer <= 0.0f) + if (affliction.AppliedAsFailedTreatmentTime > Timing.TotalTime - 1.0 && afflictionIcon.FlashTimer <= 0.0f) { - btn.Flash(GUI.Style.Red); + afflictionIcon.Flash(GUI.Style.Red); } - else if (affliction.AppliedAsSuccessfulTreatmentTime > Timing.TotalTime - 1.0 && btn.FlashTimer <= 0.0f) + else if (affliction.AppliedAsSuccessfulTreatmentTime > Timing.TotalTime - 1.0 && afflictionIcon.FlashTimer <= 0.0f) { - btn.Flash(GUI.Style.Green); + afflictionIcon.Flash(GUI.Style.Green); } } - if (GUI.MouseOn != null && GUI.MouseOn.UserData is string str && str == "selectaffliction") + if (GUI.MouseOn?.UserData is Affliction) { - Affliction affliction = GUI.MouseOn.Parent.UserData as Affliction; + Affliction affliction = GUI.MouseOn?.UserData as Affliction; if (afflictionTooltip == null || afflictionTooltip.UserData != affliction) { @@ -802,6 +799,8 @@ namespace Barotrauma currentDisplayedLimb = selectedLimb; } + UpdateAfflictionInfos(displayedAfflictions.Select(d => d.affliction)); + foreach (GUIComponent component in recommendedTreatmentContainer.Content.Children) { var treatmentButton = component.GetChild(); @@ -857,15 +856,6 @@ namespace Barotrauma selectedLimbIndex = highlightedLimbIndex; } } - - if (draggingMed != null) - { - if (!PlayerInput.PrimaryMouseButtonHeld()) - { - OnItemDropped(draggingMed.UserData as Item, ignoreMousePos: false); - draggingMed = null; - } - } } else { @@ -1157,8 +1147,6 @@ namespace Barotrauma { CreateRecommendedTreatments(); } - - UpdateAfflictionInfos(displayedAfflictions.Select(d => d.affliction)); } private void CreateAfflictionInfos(IEnumerable afflictions) @@ -1173,28 +1161,40 @@ namespace Barotrauma { displayedAfflictions.Add((affliction, affliction.Strength)); - var child = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.3f), afflictionIconContainer.Content.RectTransform, Anchor.TopCenter)) + var frame = new GUIButton(new RectTransform(new Vector2(1.0f, 0.25f), afflictionIconContainer.Content.RectTransform), style: "ListBoxElement") { - Stretch = true, - UserData = affliction - }; - - var button = new GUIButton(new RectTransform(new Vector2(1.0f, 0.9f), child.RectTransform), style: null) - { - Color = Color.Gray.Multiply(0.1f).Opaque(), - HoverColor = Color.Gray.Multiply(0.4f).Opaque(), - SelectedColor = Color.Gray.Multiply(0.25f).Opaque(), - PressedColor = Color.Gray.Multiply(0.2f).Opaque(), - UserData = "selectaffliction", + UserData = affliction, OnClicked = SelectAffliction }; + new GUIFrame(new RectTransform(Vector2.One, frame.RectTransform), style: "GUIFrameListBox") { CanBeFocused = false }; + + var content = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.85f), frame.RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) + { + Stretch = true, + CanBeFocused = false + }; + + var progressbarBg = new GUIProgressBar(new RectTransform(new Vector2(1.0f, 0.18f), content.RectTransform), 0.0f, GUI.Style.Green, style: "GUIAfflictionBar") + { + UserData = "afflictionstrengthprediction", + CanBeFocused = false + }; + new GUIProgressBar(new RectTransform(Vector2.One, progressbarBg.RectTransform), 0.0f, Color.Transparent, showFrame: false, style: "GUIAfflictionBar") + { + UserData = "afflictionstrength", + CanBeFocused = false + }; + + //spacing + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.15f), content.RectTransform), style: null) { CanBeFocused = false }; + if (affliction == mostSevereAffliction) { - buttonToSelect = button; + buttonToSelect = frame; } - var afflictionIcon = new GUIImage(new RectTransform(Vector2.One * 0.8f, button.RectTransform, Anchor.Center), affliction.Prefab.Icon, scaleToFit: true) + var afflictionIcon = new GUIImage(new RectTransform(Vector2.One * 0.8f, content.RectTransform), affliction.Prefab.Icon, scaleToFit: true) { Color = GetAfflictionIconColor(affliction), CanBeFocused = false @@ -1203,32 +1203,22 @@ namespace Barotrauma afflictionIcon.HoverColor = Color.Lerp(afflictionIcon.Color, Color.White, 0.6f); afflictionIcon.SelectedColor = Color.Lerp(afflictionIcon.Color, Color.White, 0.5f); - float afflictionVitalityDecrease = affliction.GetVitalityDecrease(this); - - Color afflictionEffectColor = Color.White; - if (afflictionVitalityDecrease > 0.0f) + var nameText = new GUITextBlock(new RectTransform(new Vector2(1.1f, 0.0f), content.RectTransform), + affliction.Prefab.Name, font: GUI.SmallFont, textAlignment: Alignment.BottomCenter) { - afflictionEffectColor = GUI.Style.Red; - } - else if (afflictionVitalityDecrease < 0.0f) - { - afflictionEffectColor = GUI.Style.Green; - } - - var nameText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), child.RectTransform), - affliction.Prefab.Name, font: GUI.SmallFont, textAlignment: Alignment.Center, style: "GUIToolTip"); + CanBeFocused = false + }; nameText.Text = ToolBox.LimitString(nameText.Text, nameText.Font, nameText.Rect.Width); - nameText.RectTransform.MinSize = new Point(0, (int)(nameText.TextSize.Y * 1.25f)); - - new GUIProgressBar(new RectTransform(new Vector2(1.0f, 0.15f), child.RectTransform), 0.0f, afflictionEffectColor, style: "GUIAfflictionBar") + nameText.RectTransform.MinSize = new Point(0, (int)(nameText.TextSize.Y)); + nameText.RectTransform.SizeChanged += () => { - UserData = "afflictionstrength" + nameText.Text = ToolBox.LimitString(nameText.Text, nameText.Font, nameText.Rect.Width); }; - child.Recalculate(); + content.Recalculate(); } - buttonToSelect?.OnClicked(buttonToSelect, "selectaffliction"); + buttonToSelect?.OnClicked(buttonToSelect, buttonToSelect.UserData); afflictionIconContainer.RecalculateChildren(); } @@ -1448,11 +1438,52 @@ namespace Barotrauma private void UpdateAfflictionInfos(IEnumerable afflictions) { + var potentialTreatment = Inventory.DraggingItems.FirstOrDefault(); + if (potentialTreatment == null && GUI.MouseOn?.UserData is ItemPrefab itemPrefab) + { + potentialTreatment = Character.Controlled.Inventory.AllItems.FirstOrDefault(it => it.prefab == itemPrefab); + } + potentialTreatment ??= Inventory.SelectedSlot?.Item; + foreach (Affliction affliction in afflictions) { + float afflictionVitalityDecrease = affliction.GetVitalityDecrease(this); + Color afflictionEffectColor = Color.White; + if (afflictionVitalityDecrease > 0.0f) + { + afflictionEffectColor = GUI.Style.Red; + } + else if (afflictionVitalityDecrease < 0.0f) + { + afflictionEffectColor = GUI.Style.Green; + } + var child = afflictionIconContainer.Content.FindChild(affliction); - var afflictionStrengthBar = child.GetChildByUserData("afflictionstrength") as GUIProgressBar; + + var afflictionStrengthPredictionBar = child.GetChild().GetChildByUserData("afflictionstrengthprediction") as GUIProgressBar; + afflictionStrengthPredictionBar.BarSize = 0.0f; + var afflictionStrengthBar = afflictionStrengthPredictionBar.GetChildByUserData("afflictionstrength") as GUIProgressBar; afflictionStrengthBar.BarSize = affliction.Strength / affliction.Prefab.MaxStrength; + afflictionStrengthBar.Color = afflictionEffectColor; + + float afflictionStrengthPrediction = GetAfflictionStrengthPrediction(potentialTreatment, affliction); + if (!MathUtils.NearlyEqual(afflictionStrengthPrediction, affliction.Strength)) + { + float t = (float)Math.Max(0.5f, (Math.Sin(Timing.TotalTime * 5) + 1.0f) / 2.0f); + if (afflictionStrengthPrediction < affliction.Strength) + { + afflictionStrengthBar.Color = afflictionEffectColor; + afflictionStrengthPredictionBar.Color = GUI.Style.Blue * t; + afflictionStrengthPredictionBar.BarSize = afflictionStrengthBar.BarSize; + afflictionStrengthBar.BarSize = afflictionStrengthPrediction / affliction.Prefab.MaxStrength; + } + else + { + afflictionStrengthPredictionBar.Color = Color.Red * t; + afflictionStrengthPredictionBar.BarSize = afflictionStrengthPrediction / affliction.Prefab.MaxStrength; + } + } + if (afflictionTooltip != null && afflictionTooltip.UserData == affliction) { UpdateAfflictionInfo(afflictionTooltip.Content, affliction); @@ -1460,6 +1491,32 @@ namespace Barotrauma } } + private float GetAfflictionStrengthPrediction(Item item, Affliction affliction) + { + float strength = affliction.Strength; + if (item == null) { return strength; } + + foreach (ItemComponent ic in item.Components) + { + if (ic.statusEffectLists == null) { continue; } + if (!ic.statusEffectLists.TryGetValue(ActionType.OnUse, out List statusEffects)) { continue; } + foreach (StatusEffect effect in statusEffects) + { + foreach (var reduceAffliction in effect.ReduceAffliction) + { + if (reduceAffliction.affliction != affliction.Identifier && reduceAffliction.affliction != affliction.Prefab.AfflictionType) { continue; } + strength -= reduceAffliction.amount * (effect.Duration > 0 ? effect.Duration : 1.0f); + } + foreach (var addAffliction in effect.Afflictions) + { + if (addAffliction.Prefab != affliction.Prefab) { continue; } + strength += addAffliction.Strength * (effect.Duration > 0 ? effect.Duration : 1.0f); + } + } + } + return strength; + } + private void UpdateAfflictionInfo(GUIComponent parent, Affliction affliction) { var labelContainer = parent.GetChildByUserData("label"); @@ -1722,13 +1779,6 @@ namespace Barotrauma Color.LightGray * 0.5f, width: 4); } } - - if (draggingMed != null) - { - GUIImage itemImage = draggingMed.GetChild(); - float scale = Math.Min(40.0f / itemImage.Sprite.size.X, 40.0f / itemImage.Sprite.size.Y); - itemImage.Sprite.Draw(spriteBatch, PlayerInput.MousePosition, itemImage.Color, 0, scale); - } } private void DrawLimbAfflictionIcon(SpriteBatch spriteBatch, Affliction affliction, float iconScale, ref Vector2 iconPos) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs index d221c34b7..fb1038f97 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs @@ -679,6 +679,14 @@ namespace Barotrauma { clr = clr.Multiply(character.Info.SkinColor); } + if (character.CharacterHealth.FaceTint.A > 0 && type == LimbType.Head) + { + clr = Color.Lerp(clr, character.CharacterHealth.FaceTint.Opaque(), character.CharacterHealth.FaceTint.A / 255.0f); + } + if (character.CharacterHealth.BodyTint.A > 0) + { + clr = Color.Lerp(clr, character.CharacterHealth.BodyTint.Opaque(), character.CharacterHealth.BodyTint.A / 255.0f); + } } Color color = new Color((byte)(clr.R * brightness), (byte)(clr.G * brightness), (byte)(clr.B * brightness), clr.A); Color blankColor = new Color(brightness, brightness, brightness, 1); @@ -720,6 +728,7 @@ namespace Barotrauma } body.UpdateDrawPosition(); + float depthStep = 0.000001f; if (!hideLimb) { @@ -794,7 +803,7 @@ namespace Barotrauma } else { - body.Draw(spriteBatch, conditionalSprite.Sprite, color, null, Scale * TextureScale, Params.MirrorHorizontally, Params.MirrorVertically); + body.Draw(spriteBatch, conditionalSprite.Sprite, color, depth: activeSprite.Depth - (depthStep * 50), Scale * TextureScale, Params.MirrorHorizontally, Params.MirrorVertically); } } } @@ -809,7 +818,7 @@ namespace Barotrauma new Vector2(body.DrawPosition.X, -body.DrawPosition.Y), color * Math.Min(damageOverlayStrength, 1.0f), activeSprite.Origin, -body.DrawRotation, - Scale, spriteEffect, activeSprite.Depth - 0.0000015f); + Scale, spriteEffect, activeSprite.Depth - (depthStep * 90)); } foreach (var decorativeSprite in DecorativeSprites) { @@ -827,9 +836,8 @@ namespace Barotrauma Vector2 transformedOffset = new Vector2(ca * offset.X + sa * offset.Y, -sa * offset.X + ca * offset.Y); decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(body.DrawPosition.X + transformedOffset.X, -(body.DrawPosition.Y + transformedOffset.Y)), c, -body.Rotation + rotation, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, spriteEffect, - depth: decorativeSprite.Sprite.Depth); + depth: activeSprite.Depth - (depthStep * 100)); } - float depthStep = 0.000001f; float step = depthStep; WearableSprite onlyDrawable = wearingItems.Find(w => w.HideOtherWearables); if (Params.MirrorHorizontally) diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index 988125af4..90c057cab 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -690,6 +690,7 @@ namespace Barotrauma AssignRelayToServer("readycheck", true); AssignRelayToServer("givetalent", true); + AssignRelayToServer("unlocktalents", true); AssignRelayToServer("giveexperience", true); AssignOnExecute("control", (string[] args) => @@ -1099,9 +1100,35 @@ namespace Barotrauma commands.Add(new Command("load|loadsub", "load [submarine name]: Load a submarine.", (string[] args) => { - if (args.Length == 0) return; - SubmarineInfo subInfo = new SubmarineInfo(string.Join(" ", args)); + if (args.Length == 0) { return; } + + if (GameMain.GameSession != null) + { + ThrowError("The loadsub command cannot be used when a round is running. You should probably be using spawnsub instead."); + return; + } + + string name = string.Join(" ", args); + SubmarineInfo subInfo = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => name.Equals(s.Name, StringComparison.OrdinalIgnoreCase)); + if (subInfo == null) + { + string path = Path.Combine(SubmarineInfo.SavePath, name); + if (!File.Exists(path)) + { + ThrowError($"Could not find a submarine with the name \"{name}\" or in the path {path}."); + return; + } + subInfo = new SubmarineInfo(path); + } + Submarine.Load(subInfo, true); + }, + () => + { + return new string[][] + { + SubmarineInfo.SavedSubmarines.Select(s => s.Name).ToArray() + }; })); commands.Add(new Command("cleansub", "", (string[] args) => diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AlienRuinMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AlienRuinMission.cs new file mode 100644 index 000000000..586f9c37a --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/AlienRuinMission.cs @@ -0,0 +1,31 @@ +using Barotrauma.Networking; + +namespace Barotrauma +{ + partial class AlienRuinMission : Mission + { + public override void ClientReadInitial(IReadMessage msg) + { + existingTargets.Clear(); + spawnedTargets.Clear(); + allTargets.Clear(); + ushort existingTargetsCount = msg.ReadUInt16(); + for (int i = 0; i < existingTargetsCount; i++) + { + ushort targetId = msg.ReadUInt16(); + if (targetId == Entity.NullEntityID) { continue; } + Entity target = Entity.FindEntityByID(targetId); + if (target == null) { continue; } + existingTargets.Add(target); + allTargets.Add(target); + } + ushort spawnedTargetsCount = msg.ReadUInt16(); + for (int i = 0; i < spawnedTargetsCount; i++) + { + var enemy = Character.ReadSpawnData(msg); + existingTargets.Add(enemy); + allTargets.Add(enemy); + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs index 92d36b5fe..5af7dd9cf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/Mission.cs @@ -3,6 +3,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; namespace Barotrauma { @@ -14,6 +15,10 @@ namespace Barotrauma get { return shownMessages; } } + public bool DisplayTargetHudIcons => Prefab.DisplayTargetHudIcons; + + public virtual IEnumerable HudIconTargets => Enumerable.Empty(); + public Color GetDifficultyColor() { int v = Difficulty ?? MissionPrefab.MinDifficulty; @@ -92,7 +97,7 @@ namespace Barotrauma }; } - public void ClientRead(IReadMessage msg) + public virtual void ClientRead(IReadMessage msg) { State = msg.ReadInt16(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs index 5bc64d50f..e3c6f8633 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs @@ -18,13 +18,54 @@ namespace Barotrauma private set; } + public bool DisplayTargetHudIcons + { + get; + private set; + } + + public float HudIconMaxDistance + { + get; + private set; + } + + public Sprite HudIcon + { + get + { + return hudIcon ?? Icon; + } + } + + public Color HudIconColor + { + get + { + return hudIconColor ?? IconColor; + } + } + + private Sprite hudIcon; + private Color? hudIconColor; + partial void InitProjSpecific(XElement element) { + DisplayTargetHudIcons = element.GetAttributeBool("displaytargethudicons", false); + HudIconMaxDistance = element.GetAttributeFloat("hudiconmaxdistance", 1000.0f); foreach (XElement subElement in element.Elements()) { - if (!subElement.Name.ToString().Equals("icon", StringComparison.OrdinalIgnoreCase)) { continue; } - Icon = new Sprite(subElement); - IconColor = subElement.GetAttributeColor("color", Color.White); + string name = subElement.Name.ToString(); + if (name.Equals("icon", StringComparison.OrdinalIgnoreCase)) + { + Icon = new Sprite(subElement); + IconColor = subElement.GetAttributeColor("color", Color.White); + } + else if (name.Equals("hudicon", StringComparison.OrdinalIgnoreCase)) + { + hudIcon = new Sprite(subElement); + hudIconColor = subElement.GetAttributeColor("color"); + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/ScanMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/ScanMission.cs new file mode 100644 index 000000000..bbf3c18f6 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/ScanMission.cs @@ -0,0 +1,66 @@ +using Barotrauma.Networking; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + partial class ScanMission : Mission + { + public override IEnumerable HudIconTargets + { + get + { + if (State == 0) + { + return scanTargets.Where(kvp => !kvp.Value).Select(kvp => kvp.Key); + } + else + { + return Enumerable.Empty(); + } + } + } + + public override void ClientReadInitial(IReadMessage msg) + { + startingItems.Clear(); + ushort itemCount = msg.ReadUInt16(); + for (int i = 0; i < itemCount; i++) + { + startingItems.Add(Item.ReadSpawnData(msg)); + } + if (startingItems.Contains(null)) + { + throw new Exception($"Error in ScanMission.ClientReadInitial: item list contains null (mission: {Prefab.Identifier})"); + } + if (startingItems.Count != itemCount) + { + throw new Exception($"Error in ScanMission.ClientReadInitial: item count does not match the server count ({itemCount} != {startingItems.Count}, mission: {Prefab.Identifier})"); + } + scanners.Clear(); + GetScanners(); + ClientReadScanTargetStatus(msg); + } + + public override void ClientRead(IReadMessage msg) + { + base.ClientRead(msg); + ClientReadScanTargetStatus(msg); + } + + private void ClientReadScanTargetStatus(IReadMessage msg) + { + scanTargets.Clear(); + byte targetsToScan = msg.ReadByte(); + for (int i = 0; i < targetsToScan; i++) + { + ushort id = msg.ReadUInt16(); + bool scanned = msg.ReadBoolean(); + Entity entity = Entity.FindEntityByID(id); + scanTargets.Add(entity as WayPoint, scanned); + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs index 2d0e44691..f3957f392 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs @@ -627,7 +627,7 @@ namespace Barotrauma { if (child == Content || child == ScrollBar || child == ContentBackground) { continue; } child.AddToGUIUpdateList(ignoreChildren, order); - } + } } foreach (GUIComponent child in Content.Children) @@ -656,7 +656,7 @@ namespace Barotrauma OnAddedToGUIUpdateList?.Invoke(this); return; } - + int lastVisible = 0; for (int i = 0; i < Content.CountChildren; i++) { @@ -700,6 +700,8 @@ namespace Barotrauma } } + public void ForceUpdate() => Update((float)Timing.Step); + protected override void Update(float deltaTime) { if (!Visible) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs index 934f6b001..114c2282e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -595,10 +595,10 @@ namespace Barotrauma private string GetPlayerBalanceText() => GetCurrencyFormatted(PlayerMoney); - private GUILayoutGroup CreateDealsGroup(GUIListBox parentList) + private GUILayoutGroup CreateDealsGroup(GUIListBox parentList, int elementCount = 4) { var elementHeight = (int)(GUI.yScale * 80); - var frame = new GUIFrame(new RectTransform(new Point(parentList.Content.Rect.Width, 4 * elementHeight + 3), parent: parentList.Content.RectTransform), style: null); + var frame = new GUIFrame(new RectTransform(new Point(parentList.Content.Rect.Width, elementCount * elementHeight + 3), parent: parentList.Content.RectTransform), style: null); frame.UserData = "deals"; var dealsGroup = new GUILayoutGroup(new RectTransform(Vector2.One, frame.RectTransform, anchor: Anchor.Center), childAnchor: Anchor.TopCenter); var dealsHeader = new GUILayoutGroup(new RectTransform(new Point((int)(0.95f * parentList.Content.Rect.Width), elementHeight), parent: dealsGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); @@ -726,6 +726,8 @@ namespace Barotrauma FilterStoreItems(category, searchBox.Text); } + int prevDailySpecialCount; + private void RefreshStoreBuyList() { float prevBuyListScroll = storeBuyList.BarScroll; @@ -734,11 +736,14 @@ namespace Barotrauma bool hasPermissions = HasPermissions; HashSet existingItemFrames = new HashSet(); - if ((storeDailySpecialsGroup != null) != CurrentLocation.DailySpecials.Any()) + int dailySpecialCount = CurrentLocation?.DailySpecials.Count() ?? 3; + + if ((storeDailySpecialsGroup != null) != CurrentLocation.DailySpecials.Any() || dailySpecialCount != prevDailySpecialCount) { - if (storeDailySpecialsGroup == null) + if (storeDailySpecialsGroup == null || dailySpecialCount != prevDailySpecialCount) { - storeDailySpecialsGroup = CreateDealsGroup(storeBuyList); + storeBuyList.RemoveChild(storeDailySpecialsGroup?.Parent); + storeDailySpecialsGroup = CreateDealsGroup(storeBuyList, 1 + dailySpecialCount); storeDailySpecialsGroup.Parent.SetAsFirstChild(); } else @@ -747,6 +752,7 @@ namespace Barotrauma storeDailySpecialsGroup = null; } storeBuyList.RecalculateChildren(); + prevDailySpecialCount = dailySpecialCount; } foreach (PurchasedItem item in CurrentLocation.StoreStock) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index d62df1090..7af53c66b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -1320,7 +1320,7 @@ namespace Barotrauma if (!(characterLayout is null)) { GUILayoutGroup characterCloseButtonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), characterLayout.RectTransform), childAnchor: Anchor.BottomRight); - new GUIButton(new RectTransform(new Vector2(0.4f, 1f), characterCloseButtonLayout.RectTransform), TextManager.Get("close")) + new GUIButton(new RectTransform(new Vector2(0.4f, 1f), characterCloseButtonLayout.RectTransform), TextManager.Get("ApplySettingsButton")) //TODO: Is this text appropriate for this circumstance for all languages? { OnClicked = (button, o) => { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index ad9693350..9134bfff2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -922,8 +922,7 @@ namespace Barotrauma } //open the pause menu if not controlling a character OR if the character has no UIs active that can be closed with ESC else if ((Character.Controlled == null || !itemHudActive()) - //TODO: do we need to check Inventory.SelectedSlot? - && Inventory.SelectedSlot == null && CharacterHealth.OpenHealthWindow == null + && CharacterHealth.OpenHealthWindow == null && !CrewManager.IsCommandInterfaceOpen && !(Screen.Selected is SubEditorScreen editor && !editor.WiringMode && Character.Controlled?.SelectedConstruction != null)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs index f42c29829..d3444ed00 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs @@ -1837,6 +1837,7 @@ namespace Barotrauma ic.ParseMsg(); } } + CharacterHUD.ShouldRecreateHudTexts = true; } private void ApplySettings() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs index 69bc06a85..ebd3e27a8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/IdCard.cs @@ -1,13 +1,201 @@ -using Microsoft.Xna.Framework; +using System; +using Microsoft.Xna.Framework; using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Xml.Linq; +using Barotrauma.Extensions; +using Barotrauma.IO; namespace Barotrauma.Items.Components { partial class IdCard { - public Sprite StoredPortrait; - public Vector2 StoredSheetIndex; - public JobPrefab StoredJobPrefab; - public List StoredAttachments; + public struct OwnerAppearance + { + public Sprite Portrait; + public Vector2 SheetIndex; + public JobPrefab JobPrefab; + public List Attachments; + public Color HairColor; + public Color FacialHairColor; + public Color SkinColor; + + public void ExtractJobPrefab(string[] tags) + { + string jobIdTag = tags.FirstOrDefault(s => s.StartsWith("jobid:")); + + if (jobIdTag != null && jobIdTag.Length > 6) + { + string jobId = jobIdTag.Substring(6); + if (jobId != string.Empty) + { + JobPrefab = JobPrefab.Get(jobId); + } + } + } + + public void ExtractAppearance(CharacterInfo characterInfo, string[] tags) + { + Gender disguisedGender = Gender.None; + Race disguisedRace = Race.None; + int disguisedHeadSpriteId = -1; + int disguisedHairIndex = -1; + int disguisedBeardIndex = -1; + int disguisedMoustacheIndex = -1; + int disguisedFaceAttachmentIndex = -1; + Color hairColor = Color.Black; + Color facialHairColor = Color.Black; + Color skinColor = Color.Black; + + foreach (string tag in tags) + { + string[] s = tag.Split(':'); + + switch (s[0].ToLowerInvariant()) + { + case "haircolor": + hairColor = XMLExtensions.ParseColor(s[1]); + break; + + case "facialhaircolor": + facialHairColor = XMLExtensions.ParseColor(s[1]); + break; + + case "skincolor": + skinColor = XMLExtensions.ParseColor(s[1]); + break; + + case "gender": + Enum.TryParse(s[1], ignoreCase: true, out disguisedGender); + break; + + case "race": + Enum.TryParse(s[1], ignoreCase: true, out disguisedRace); + break; + + case "headspriteid": + int.TryParse(s[1], NumberStyles.Any, CultureInfo.InvariantCulture, out disguisedHeadSpriteId); + 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(";"); + SheetIndex = new Vector2(float.Parse(vectorValues[0]), float.Parse(vectorValues[1])); + break; + } + } + + if ((characterInfo.HasGenders && disguisedGender == Gender.None) + || (characterInfo.HasRaces && disguisedRace == Race.None) + || disguisedHeadSpriteId <= 0) + { + Portrait = null; + Attachments = null; + return; + } + + foreach (XElement limbElement in characterInfo.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.ToString().ToLowerInvariant()); + spritePath = spritePath.Replace("[RACE]", disguisedRace.ToString().ToLowerInvariant()); + spritePath = spritePath.Replace("[HEADID]", disguisedHeadSpriteId.ToString()); + + 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; } + Portrait = new Sprite(spriteElement, "", file) { RelativeOrigin = Vector2.Zero }; + break; + } + + break; + } + + if (characterInfo.Wearables != null) + { + float baldnessChance = disguisedGender == Gender.Female ? 0.05f : 0.2f; + + List createElementList(WearableType wearableType, float emptyCommonness = 1.0f) + => CharacterInfo.AddEmpty( + characterInfo.FilterByTypeAndHeadID( + characterInfo.FilterElementsByGenderAndRace(characterInfo.Wearables, disguisedGender, disguisedRace), + wearableType, disguisedHeadSpriteId), + wearableType, emptyCommonness); + + var disguisedHairs = createElementList(WearableType.Hair, baldnessChance); + var disguisedBeards = createElementList(WearableType.Beard); + var disguisedMoustaches = createElementList(WearableType.Moustache); + var disguisedFaceAttachments = createElementList(WearableType.FaceAttachment); + + XElement getElementFromList(List list, int index) + => CharacterInfo.IsValidIndex(index, list) + ? list[index] + : characterInfo.GetRandomElement(list); + + var disguisedHairElement = getElementFromList(disguisedHairs, disguisedHairIndex); + var disguisedBeardElement = getElementFromList(disguisedBeards, disguisedBeardIndex); + var disguisedMoustacheElement = getElementFromList(disguisedMoustaches, disguisedMoustacheIndex); + var disguisedFaceAttachmentElement = getElementFromList(disguisedFaceAttachments, disguisedFaceAttachmentIndex); + + Attachments = new List(); + + void loadAttachments(List attachments, XElement element, WearableType wearableType) + { + foreach (var s in element?.Elements("sprite") ?? Enumerable.Empty()) + { + attachments.Add(new WearableSprite(s, wearableType)); + } + } + + loadAttachments(Attachments, disguisedFaceAttachmentElement, WearableType.FaceAttachment); + loadAttachments(Attachments, disguisedBeardElement, WearableType.Beard); + loadAttachments(Attachments, disguisedMoustacheElement, WearableType.Moustache); + loadAttachments(Attachments, disguisedHairElement, WearableType.Hair); + + loadAttachments(Attachments, + characterInfo.OmitJobInPortraitClothing + ? JobPrefab.NoJobElement?.Element("PortraitClothing") + : JobPrefab?.ClothingElement, + WearableType.JobIndicator); + } + + HairColor = hairColor; + FacialHairColor = facialHairColor; + SkinColor = skinColor; + } + } + + public OwnerAppearance StoredOwnerAppearance = default; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs index d2521a444..da5c38759 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs @@ -289,7 +289,7 @@ namespace Barotrauma.Items.Components } else { - Matrix transform = Matrix.CreateRotationZ(item.body.Rotation); + Matrix transform = Matrix.CreateRotationZ(item.body.DrawRotation); if (item.body.Dir == -1.0f) { transformedItemPos.X = -transformedItemPos.X; @@ -300,7 +300,7 @@ namespace Barotrauma.Items.Components transformedItemPos = Vector2.Transform(transformedItemPos, transform); transformedItemInterval = Vector2.Transform(transformedItemInterval, transform); transformedItemIntervalHorizontal = Vector2.Transform(transformedItemIntervalHorizontal, transform); - transformedItemPos += item.DrawPosition; + transformedItemPos += item.body.DrawPosition; } Vector2 currentItemPos = transformedItemPos; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index fabc40b20..69b1337f3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -380,9 +380,20 @@ namespace Barotrauma.Items.Components if (requiredItem.UseCondition && requiredItem.MinCondition < 1.0f) { - GUI.DrawRectangle(spriteBatch, new Rectangle(slotRect.X, slotRect.Bottom - 8, slotRect.Width, 8), Color.Black * 0.8f, true); + DrawConditionBar(spriteBatch, requiredItem.MinCondition); + } + else if (requiredItem.MaxCondition < 1.0f) + { + DrawConditionBar(spriteBatch, requiredItem.MaxCondition); + } + + void DrawConditionBar(SpriteBatch sb, float condition) + { + int spacing = GUI.IntScale(4); + int height = GUI.IntScale(10); + GUI.DrawRectangle(spriteBatch, new Rectangle(slotRect.X + spacing, slotRect.Bottom - spacing - height, slotRect.Width - spacing * 2, height), Color.Black * 0.8f, true); GUI.DrawRectangle(spriteBatch, - new Rectangle(slotRect.X, slotRect.Bottom - 8, (int)(slotRect.Width * requiredItem.MinCondition), 8), + new Rectangle(slotRect.X + spacing, slotRect.Bottom - spacing - height, (int)((slotRect.Width - spacing * 2) * condition), height), GUI.Style.Green * 0.8f, true); } @@ -395,6 +406,10 @@ namespace Barotrauma.Items.Components { toolTipText += " " + (int)Math.Round(requiredItem.MinCondition * 100) + "%"; } + else if(requiredItem.MaxCondition < 1.0f) + { + toolTipText += " 0-" + (int)Math.Round(requiredItem.MaxCondition * 100) + "%"; + } else if (requiredItem.MaxCondition <= 0.0f) { toolTipText = TextManager.GetWithVariable("displayname.emptyitem", "[itemname]", toolTipText); @@ -649,8 +664,13 @@ namespace Barotrauma.Items.Components { foreach (GUIComponent child in itemList.Content.Children) { - var itemPrefab = child.UserData as FabricationRecipe; - if (itemPrefab == null) continue; + if (!(child.UserData is FabricationRecipe itemPrefab)) { continue; } + + if (itemPrefab != selectedItem && + (child.Rect.Y > itemList.Rect.Bottom || child.Rect.Bottom < itemList.Rect.Y)) + { + continue; + } bool canBeFabricated = CanBeFabricated(itemPrefab, availableIngredients, character); if (itemPrefab == selectedItem) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index 780de4f8b..c708f6538 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -132,7 +132,7 @@ namespace Barotrauma.Items.Components private bool isConnectedToSteering; - private static string caveLabel; + private static string caveLabel, ruinLabel; private bool AllowUsingMineralScanner => HasMineralScanner && !isConnectedToSteering; @@ -880,7 +880,7 @@ namespace Barotrauma.Items.Components foreach (AITarget aiTarget in AITarget.List) { - if (!aiTarget.Enabled) { continue; } + if (aiTarget.InDetectable) { continue; } if (string.IsNullOrEmpty(aiTarget.SonarLabel) || aiTarget.SoundRange <= 0.0f) { continue; } if (Vector2.DistanceSquared(aiTarget.WorldPosition, transducerCenter) < aiTarget.SoundRange * aiTarget.SoundRange) @@ -1234,7 +1234,7 @@ namespace Barotrauma.Items.Components foreach (AITarget aiTarget in AITarget.List) { float disruption = aiTarget.Entity is Character c ? c.Params.SonarDisruption : aiTarget.SonarDisruption; - if (disruption <= 0.0f || !aiTarget.Enabled) { continue; } + if (disruption <= 0.0f || aiTarget.InDetectable) { continue; } float distSqr = Vector2.DistanceSquared(aiTarget.WorldPosition, pingSource); if (distSqr > worldPingRadiusSqr) { continue; } float disruptionDist = (float)Math.Sqrt(distSqr); @@ -1359,28 +1359,6 @@ namespace Barotrauma.Items.Components blipType : cell.IsDestructible ? BlipType.Destructible : BlipType.Default); } } - - foreach (RuinGeneration.Ruin ruin in Level.Loaded.Ruins) - { - if (!MathUtils.CircleIntersectsRectangle(pingSource, range, ruin.Area)) continue; - - foreach (var ruinShape in ruin.RuinShapes) - { - foreach (RuinGeneration.Line wall in ruinShape.Walls) - { - float cellDot = Vector2.Dot( - Vector2.Normalize(ruinShape.Center - pingSource), - Vector2.Normalize((wall.A + wall.B) / 2.0f - ruinShape.Center)); - if (cellDot > 0) continue; - - CreateBlipsForLine( - wall.A, wall.B, - pingSource, transducerPos, - pingRadius, prevPingRadius, - 100.0f, 1000.0f, range, pingStrength, passive); - } - } - } } foreach (Item item in Item.ItemList) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs index fcdf7f34c..01db669ab 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs @@ -21,6 +21,8 @@ namespace Barotrauma.Items.Components private GUITextBlock progressBarOverlayText; + private GUILayoutGroup extraButtonContainer; + private readonly List particleEmitters = new List(); //the corresponding particle emitter is active when the condition is within this range private readonly List particleEmitterConditionRanges = new List(); @@ -145,10 +147,16 @@ namespace Barotrauma.Items.Components progressBarHolder.RectTransform.MinSize = RepairButton.RectTransform.MinSize; RepairButton.RectTransform.MinSize = new Point((int)(RepairButton.TextBlock.TextSize.X * 1.2f), RepairButton.RectTransform.MinSize.Y); + extraButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), paddedFrame.RectTransform), isHorizontal: true) + { + IgnoreLayoutGroups = true, + Stretch = true, + AbsoluteSpacing = GUI.IntScale(5) + }; sabotageButtonText = TextManager.Get("SabotageButton"); sabotagingText = TextManager.Get("Sabotaging"); - SabotageButton = new GUIButton(new RectTransform(new Vector2(0.8f, 0.15f), paddedFrame.RectTransform, Anchor.BottomCenter), sabotageButtonText, style: "GUIButtonSmall") + SabotageButton = new GUIButton(new RectTransform(Vector2.One, extraButtonContainer.RectTransform), sabotageButtonText, style: "GUIButtonSmall") { IgnoreLayoutGroups = true, Visible = false, @@ -160,9 +168,9 @@ namespace Barotrauma.Items.Components } }; - tinkerButtonText = "Tinker"; - tinkeringText = "Tinkering"; - TinkerButton = new GUIButton(new RectTransform(new Vector2(0.8f, 0.15f), paddedFrame.RectTransform, Anchor.BottomCenter), tinkerButtonText) + tinkerButtonText = TextManager.Get("TinkerButton", returnNull: true) ?? "Tinker"; + tinkeringText = TextManager.Get("Tinkering", returnNull: true) ?? "Tinkering"; + TinkerButton = new GUIButton(new RectTransform(Vector2.One, extraButtonContainer.RectTransform), tinkerButtonText, style: "GUIButtonSmall") { IgnoreLayoutGroups = true, Visible = false, @@ -173,6 +181,8 @@ namespace Barotrauma.Items.Components return true; } }; + + extraButtonContainer.RectTransform.MinSize = new Point(0, SabotageButton.RectTransform.MinSize.Y); } partial void UpdateProjSpecific(float deltaTime) @@ -274,6 +284,10 @@ namespace Barotrauma.Items.Components tinkeringText + new string('.', ((int)(Timing.TotalTime * 2.0f) % 3) + 1); System.Diagnostics.Debug.Assert(GuiFrame.GetChild(0) is GUILayoutGroup, "Repair UI hierarchy has changed, could not find skill texts"); + + extraButtonContainer.Visible = SabotageButton.Visible || TinkerButton.Visible; + extraButtonContainer.IgnoreLayoutGroups = !extraButtonContainer.Visible; + foreach (GUIComponent c in GuiFrame.GetChild(0).Children) { if (!(c.UserData is Skill skill)) continue; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs index f68e5f37d..d31ee696e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs @@ -31,6 +31,9 @@ namespace Barotrauma.Items.Components set; } + [Serialize("0.5,0.5)", false)] + public Vector2 Origin { get; set; } = new Vector2(0.5f, 0.5f); + public Vector2 DrawSize { get @@ -57,7 +60,6 @@ namespace Barotrauma.Items.Components sourcePos = sourceLimb.body.DrawPosition; } return sourcePos; - } partial void InitProjSpecific(XElement element) @@ -87,7 +89,8 @@ namespace Barotrauma.Items.Components startPos.Y = -startPos.Y; if (source is Item sourceItem) { - var turret = sourceItem?.GetComponent(); + var turret = sourceItem.GetComponent(); + var weapon = sourceItem.GetComponent(); if (turret != null) { startPos = new Vector2(sourceItem.WorldRect.X + turret.TransformedBarrelPos.X, -(sourceItem.WorldRect.Y - turret.TransformedBarrelPos.Y)); @@ -96,8 +99,21 @@ namespace Barotrauma.Items.Components startPos += new Vector2((float)Math.Cos(turret.Rotation), (float)Math.Sin(turret.Rotation)) * turret.BarrelSprite.size.Y * turret.BarrelSprite.RelativeOrigin.Y * item.Scale * 0.9f; } } + else if (weapon != null) + { + Vector2 barrelPos = FarseerPhysics.ConvertUnits.ToDisplayUnits(weapon.TransformedBarrelPos); + barrelPos.Y = -barrelPos.Y; + startPos += barrelPos * item.Scale; + } } - Vector2 endPos = new Vector2(target.DrawPosition.X, -target.DrawPosition.Y); + Vector2 endPos = new Vector2(target.DrawPosition.X, target.DrawPosition.Y); + Vector2 flippedPos = target.Sprite.size * target.Scale * (Origin - new Vector2(0.5f)); + if (target.body.Dir < 0.0f) + { + flippedPos.X = -flippedPos.X; + } + endPos += Vector2.Transform(flippedPos, Matrix.CreateRotationZ(target.body.Rotation)); + endPos.Y = -endPos.Y; if (Snapped) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Scanner.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Scanner.cs new file mode 100644 index 000000000..a44dca68e --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Scanner.cs @@ -0,0 +1,29 @@ +using Barotrauma.Networking; + +namespace Barotrauma.Items.Components +{ + partial class Scanner : ItemComponent, IServerSerializable + { + partial void UpdateProjSpecific() + { + if (Holdable != null && Holdable.Attached && (AlwaysDisplayProgressBar || DisplayProgressBar) && !IsScanCompleted) + { + Character.Controlled?.UpdateHUDProgressBar(this, + item.WorldPosition, + ScanTimer / ScanDuration, + GUI.Style.Red, GUI.Style.Green, + textTag: "progressbar.scanning"); + } + } + + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + { + bool wasScanCompletedPreviously = IsScanCompleted; + scanTimer = msg.ReadSingle(); + if (!wasScanCompletedPreviously && IsScanCompleted) + { + OnScanCompleted?.Invoke(this); + } + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ButtonTerminal.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ButtonTerminal.cs new file mode 100644 index 000000000..cb1d306f9 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ButtonTerminal.cs @@ -0,0 +1,114 @@ +using Barotrauma.Extensions; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Items.Components +{ + partial class ButtonTerminal : ItemComponent, IClientSerializable, IServerSerializable + { + private string[] terminalButtonStyles; + private GUIFrame containerHolder; + private GUIImage containerIndicator; + private GUIComponentStyle indicatorStyleRed, indicatorStyleGreen; + + partial void InitProjSpecific(XElement element) + { + terminalButtonStyles = new string[RequiredSignalCount]; + int i = 0; + foreach (var childElement in element.GetChildElements("TerminalButton")) + { + string style = childElement.GetAttributeString("style", null); + if (style == null) { continue; } + terminalButtonStyles[i++] = style; + } + indicatorStyleRed = GUI.Style.GetComponentStyle("IndicatorLightRed"); + indicatorStyleGreen = GUI.Style.GetComponentStyle("IndicatorLightGreen"); + CreateGUI(); + } + + protected override void CreateGUI() + { + var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.8f), GuiFrame.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = true, + RelativeSpacing = 0.08f + }; + paddedFrame.OnAddedToGUIUpdateList += (component) => + { + bool buttonsEnabled = AllowUsingButtons; + foreach (var child in component.Children) + { + if (!(child is GUIButton)) { continue; } + if (!(child.UserData is int)) { continue; } + child.Enabled = buttonsEnabled; + child.Children.ForEach(c => c.Enabled = buttonsEnabled); + } + bool itemsContained = Container.Inventory.AllItems.Any(); + if (itemsContained) + { + var indicatorStyle = buttonsEnabled ? indicatorStyleGreen : indicatorStyleRed; + if (containerIndicator.Style != indicatorStyle) + { + containerIndicator.ApplyStyle(indicatorStyle); + } + } + containerIndicator.OverrideState = itemsContained ? GUIComponent.ComponentState.Selected : GUIComponent.ComponentState.None; + }; + + float x = 1.0f / (1 + RequiredSignalCount); + float y = (x * paddedFrame.Rect.Width) / paddedFrame.Rect.Height; + Vector2 relativeSize = new Vector2(x, y); + + var containerSection = new GUIFrame(new RectTransform(new Vector2(x, 1.0f), paddedFrame.RectTransform), style: null); + var containerSlot = new GUIFrame(new RectTransform(new Vector2(1.0f, y), containerSection.RectTransform, anchor: Anchor.Center), style: null); + containerHolder = new GUIFrame(new RectTransform(new Vector2(1f, 1.2f), containerSlot.RectTransform, Anchor.BottomCenter), style: null); + containerIndicator = new GUIImage(new RectTransform(new Vector2(0.5f, 0.5f * y), containerSection.RectTransform, anchor: Anchor.Center) { RelativeOffset = new Vector2(0.0f, 0.05f + 0.5f * y) }, + style: "IndicatorLightRed", scaleToFit: true); + + for (int i = 0; i < RequiredSignalCount; i++) + { + var button = new GUIButton(new RectTransform(relativeSize, paddedFrame.RectTransform), style: null) + { + UserData = i, + OnClicked = (button, userData) => + { + if (GameMain.IsSingleplayer) + { + SendSignal((int)userData); + } + else + { + item.CreateClientEvent(this, new object[] { userData }); + } + return true; + } + }; + var image = new GUIImage(new RectTransform(Vector2.One, button.RectTransform), terminalButtonStyles[i], scaleToFit: true); + } + } + + protected override void OnResolutionChanged() + { + base.OnResolutionChanged(); + OnItemLoadedProjSpecific(); + } + + partial void OnItemLoadedProjSpecific() + { + Container.AllowUIOverlap = true; + Container.Inventory.RectTransform = containerHolder.RectTransform; + } + + public void ClientWrite(IWriteMessage msg, object[] extraData = null) + { + Write(msg, extraData); + } + + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + { + SendSignal(msg.ReadRangedInteger(0, Signals.Length - 1), isServerMessage: true); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index ea190c1d8..79c6f476d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -276,7 +276,7 @@ namespace Barotrauma BrokenItemSprite fadeInBrokenSprite = null; float fadeInBrokenSpriteAlpha = 0.0f; - float displayCondition = FakeBroken ? 0.0f : condition; + float displayCondition = FakeBroken ? 0.0f : ConditionPercentage; Vector2 drawOffset = Vector2.Zero; if (displayCondition < MaxCondition) { @@ -326,9 +326,14 @@ namespace Barotrauma size, color: color, textureScale: Vector2.One * Scale, depth: depth); - fadeInBrokenSprite?.Sprite.DrawTiled(spriteBatch, new Vector2(DrawPosition.X - rect.Width / 2, -(DrawPosition.Y + rect.Height / 2)) + fadeInBrokenSprite.Offset.ToVector2() * Scale, size, color: color * fadeInBrokenSpriteAlpha, - textureScale: Vector2.One * Scale, - depth: depth - 0.000001f); + + if (fadeInBrokenSprite != null) + { + float d = Math.Min(depth + (fadeInBrokenSprite.Sprite.Depth - activeSprite.Depth - 0.000001f), 0.999f); + fadeInBrokenSprite.Sprite.DrawTiled(spriteBatch, new Vector2(DrawPosition.X - rect.Width / 2, -(DrawPosition.Y + rect.Height / 2)) + fadeInBrokenSprite.Offset.ToVector2() * Scale, size, color: color * fadeInBrokenSpriteAlpha, + textureScale: Vector2.One * Scale, + depth: d); + } foreach (var decorativeSprite in Prefab.DecorativeSprites) { if (!spriteAnimState[decorativeSprite].IsActive) { continue; } @@ -357,7 +362,11 @@ namespace Barotrauma if (color.A > 0) { 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 (fadeInBrokenSprite != null) + { + float d = Math.Min(depth + (fadeInBrokenSprite.Sprite.Depth - activeSprite.Depth - 0.000001f), 0.999f); + fadeInBrokenSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + fadeInBrokenSprite.Offset.ToVector2() * Scale, color * fadeInBrokenSpriteAlpha, origin, rotationRad, Scale, activeSprite.effects, d); + } } if (Infector != null && (Infector.ParentBallastFlora.HasBrokenThrough || BallastFloraBehavior.AlwaysShowBallastFloraSprite)) { @@ -410,8 +419,11 @@ namespace Barotrauma } } body.Draw(spriteBatch, activeSprite, color, depth, Scale); - if (fadeInBrokenSprite != null) { body.Draw(spriteBatch, fadeInBrokenSprite.Sprite, color * fadeInBrokenSpriteAlpha, depth - 0.000001f, Scale); } - + if (fadeInBrokenSprite != null) + { + float d = Math.Min(depth + (fadeInBrokenSprite.Sprite.Depth - activeSprite.Depth - 0.000001f), 0.999f); + body.Draw(spriteBatch, fadeInBrokenSprite.Sprite, color * fadeInBrokenSpriteAlpha, d, Scale); + } foreach (var decorativeSprite in Prefab.DecorativeSprites) { if (!spriteAnimState[decorativeSprite].IsActive) { continue; } @@ -464,6 +476,11 @@ namespace Barotrauma if (GameMain.DebugDraw) { body?.DebugDraw(spriteBatch, Color.White); + if (GetComponent()?.PhysicsBody is PhysicsBody triggerBody) + { + triggerBody.UpdateDrawPosition(); + triggerBody.DebugDraw(spriteBatch, Color.White); + } } if (editing && IsSelected && PlayerInput.KeyDown(Keys.Space)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Ruins/RuinGenerator.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Ruins/RuinGenerator.cs index 6238f2d5d..bac02e6b7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Ruins/RuinGenerator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Ruins/RuinGenerator.cs @@ -7,14 +7,9 @@ namespace Barotrauma.RuinGeneration { public void DebugDraw(SpriteBatch spriteBatch) { - foreach (RuinShape shape in allShapes) - { - GUI.DrawString(spriteBatch, new Vector2(shape.Center.X, -shape.Center.Y - 50), shape.DistanceFromEntrance.ToString(), Color.White, Color.Black * 0.5f, font: GUI.LargeFont); - } - foreach (Line line in walls) - { - GUI.DrawLine(spriteBatch, new Vector2(line.A.X, -line.A.Y), new Vector2(line.B.X, -line.B.Y), GUI.Style.Red, 0.0f, 10); - } + Rectangle drawRect = Area; + drawRect.Y = -drawRect.Y - Area.Height; + GUI.DrawRectangle(spriteBatch, drawRect, Color.Cyan, false, 0, 6); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs index 5c2fa3df5..eccf97ae5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs @@ -846,16 +846,15 @@ namespace Barotrauma.Lights if (chList.Submarine == null) { list.AddRange(chList.List.FindAll(ch => MathUtils.CircleIntersectsRectangle(lightPos, range, ch.BoundingBox))); - } //light is outside, convexhull inside a sub else { Rectangle subBorders = chList.Submarine.Borders; subBorders.Y -= chList.Submarine.Borders.Height; - if (!MathUtils.CircleIntersectsRectangle(lightPos - chList.Submarine.WorldPosition, range, subBorders)) continue; + if (!MathUtils.CircleIntersectsRectangle(lightPos - chList.Submarine.WorldPosition, range, subBorders)) { continue; } - lightPos -= (chList.Submarine.WorldPosition - chList.Submarine.HiddenSubPosition); + lightPos -= chList.Submarine.WorldPosition - chList.Submarine.HiddenSubPosition; list.AddRange(chList.List.FindAll(ch => MathUtils.CircleIntersectsRectangle(lightPos, range, ch.BoundingBox))); } @@ -865,14 +864,6 @@ namespace Barotrauma.Lights //light is inside, convexhull outside if (chList.Submarine == null) { - lightPos += (ParentSub.WorldPosition - ParentSub.HiddenSubPosition); - HashSet visibleRuins = new HashSet(); - foreach (RuinGeneration.Ruin ruin in Level.Loaded.Ruins) - { - if (!MathUtils.CircleIntersectsRectangle(lightPos, range, ruin.Area)) { continue; } - visibleRuins.Add(ruin); - } - list.AddRange(chList.List.FindAll(ch => ch.ParentEntity?.ParentRuin != null && visibleRuins.Contains(ch.ParentEntity.ParentRuin))); continue; } //light and convexhull are both inside the same sub diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs index 2e44b1875..d86e94f81 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs @@ -1291,7 +1291,7 @@ namespace Barotrauma.Lights if (ParentSub != null) { drawPos += ParentSub.DrawPosition; } drawPos.Y = -drawPos.Y; - spriteBatch.Draw(currentTexture, drawPos, null, Color.Multiply(CurrentBrightness), -rotation, center, scale, SpriteEffects.None, 1); + spriteBatch.Draw(currentTexture, drawPos, null, Color.Multiply(CurrentBrightness), -rotation + MathHelper.ToRadians(LightSourceParams.Rotation), center, scale, SpriteEffects.None, 1); return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs index 9d19ccf5a..6d4db8e52 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs @@ -178,7 +178,6 @@ namespace Barotrauma //drawing ---------------------------------------------------- private static readonly HashSet visibleSubs = new HashSet(); - private static readonly HashSet visibleRuins = new HashSet(); public static void CullEntities(Camera cam) { visibleSubs.Clear(); @@ -198,24 +197,6 @@ namespace Barotrauma } } - visibleRuins.Clear(); - if (Level.Loaded != null) - { - foreach (Ruin ruin in Level.Loaded.Ruins) - { - Rectangle worldBorders = new Rectangle( - ruin.Area.X - 500, - ruin.Area.Y + ruin.Area.Height + 500, - ruin.Area.Width + 1000, - ruin.Area.Height + 1000); - - if (RectsOverlap(worldBorders, cam.WorldView)) - { - visibleRuins.Add(ruin); - } - } - } - if (visibleEntities == null) { visibleEntities = new List(MapEntity.mapEntityList.Count); @@ -232,10 +213,6 @@ namespace Barotrauma { if (!visibleSubs.Contains(entity.Submarine)) { continue; } } - else if (entity.ParentRuin != null) - { - if (!visibleRuins.Contains(entity.ParentRuin)) { continue; } - } if (entity.IsVisible(worldView)) { visibleEntities.Add(entity); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs index 68e65bc1f..5083fe7c1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarinePreview.cs @@ -600,6 +600,8 @@ namespace Barotrauma var prevScissorRect = GameMain.Instance.GraphicsDevice.ScissorRectangle; GameMain.Instance.GraphicsDevice.ScissorRectangle = scissorRectangle; + var prevRasterizerState = GameMain.Instance.GraphicsDevice.RasterizerState; + GameMain.Instance.GraphicsDevice.RasterizerState = GameMain.ScissorTestEnable; spriteRecorder.Render(camera); @@ -643,6 +645,7 @@ namespace Barotrauma spriteBatch.End(); GameMain.Instance.GraphicsDevice.ScissorRectangle = prevScissorRect; + GameMain.Instance.GraphicsDevice.RasterizerState = prevRasterizerState; spriteBatch.Begin(SpriteSortMode.Deferred); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs index 8e046d7ab..0696a24a2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs @@ -171,10 +171,7 @@ namespace Barotrauma { foreach (MapEntity e in mapEntityList) { - if (e.GetType() != typeof(WayPoint)) continue; - if (e == this) continue; - - if (!Submarine.RectContains(e.Rect, position)) continue; + if (!(e is WayPoint) || e == this || !e.IsHighlighted) { continue; } if (linkedTo.Contains(e)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs index 70dd8fbd8..17616623b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs @@ -287,7 +287,9 @@ namespace Barotrauma characterInfo.CreateIcon(new RectTransform(new Vector2(1.0f, 0.275f), subLayout.RectTransform)); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), subLayout.RectTransform), job.Name, job.UIColor); + var jobTextContainer = + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), subLayout.RectTransform), style: null); + var jobText = new GUITextBlock(new RectTransform(Vector2.One, jobTextContainer.RectTransform), job.Name, job.UIColor); var characterName = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.1f), subLayout.RectTransform)) { @@ -327,8 +329,11 @@ namespace Barotrauma characterName.Text = characterInfo.Name; characterName.UserData = "random"; } + + StealRandomizeButton(menu, jobTextContainer); } }; + StealRandomizeButton(CharacterMenus[i], jobTextContainer); } } @@ -381,6 +386,16 @@ namespace Barotrauma maxMissionCountContainer.Children.ForEach(c => c.ToolTip = maxMissionCountSettingHolder.ToolTip); } + private static void StealRandomizeButton(CharacterInfo.AppearanceCustomizationMenu menu, GUIComponent parent) + { + //This is just stupid + var randomizeButton = menu.RandomizeButton; + var oldButton = parent.GetChild(); + parent.RemoveChild(oldButton); + randomizeButton.RectTransform.Parent = parent.RectTransform; + randomizeButton.RectTransform.RelativeSize = Vector2.One * 1.3f; + } + private bool FinishSetup(GUIButton btn, object userdata) { if (string.IsNullOrWhiteSpace(saveNameBox.Text)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index 38e6145a0..e46694429 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -483,21 +483,18 @@ namespace Barotrauma.CharacterEditor // It's possible that the physics are disabled, because the angle widgets handle input logic in the draw method (which they shouldn't) character.AnimController.Collider.PhysEnabled = true; } - if (character.IsHumanoid) + animTestPoseToggle.Enabled = CurrentAnimation.IsGroundedAnimation; + if (animTestPoseToggle.Enabled) { - animTestPoseToggle.Enabled = CurrentAnimation.IsGroundedAnimation; - if (animTestPoseToggle.Enabled) + if (PlayerInput.KeyHit(Keys.X)) { - if (PlayerInput.KeyHit(Keys.X)) - { - SetToggle(animTestPoseToggle, !animTestPoseToggle.Selected); - } - } - else - { - animTestPoseToggle.Selected = false; + SetToggle(animTestPoseToggle, !animTestPoseToggle.Selected); } } + else + { + animTestPoseToggle.Selected = false; + } if (PlayerInput.KeyHit(InputType.Run)) { // TODO: refactor this horrible hacky index manipulation mess @@ -1072,7 +1069,7 @@ namespace Barotrauma.CharacterEditor { if (jointCreationMode == JointCreationMode.Create) { - jointEndLimb = GetClosestLimbOnSpritesheet(PlayerInput.MousePosition, l => l != null && l != jointStartLimb && l.ActiveSprite != null); + jointEndLimb = GetClosestLimbOnSpritesheet(PlayerInput.MousePosition, l => l != null && l != jointStartLimb && l.ActiveSprite != null && !l.Hidden); if (jointEndLimb != null && PlayerInput.PrimaryMouseButtonClicked()) { Vector2 anchor1 = anchor1Pos.HasValue ? anchor1Pos.Value / spriteSheetZoom : Vector2.Zero; @@ -1085,7 +1082,7 @@ namespace Barotrauma.CharacterEditor } else if (PlayerInput.PrimaryMouseButtonClicked()) { - jointStartLimb = GetClosestLimbOnSpritesheet(PlayerInput.MousePosition, l => selectedLimbs.Contains(l)); + jointStartLimb = GetClosestLimbOnSpritesheet(PlayerInput.MousePosition, l => selectedLimbs.Contains(l) && !l.Hidden); anchor1Pos = GetLimbSpritesheetRect(jointStartLimb).Center.ToVector2() - PlayerInput.MousePosition; jointCreationMode = JointCreationMode.Create; } @@ -1094,7 +1091,7 @@ namespace Barotrauma.CharacterEditor { if (jointCreationMode == JointCreationMode.Create) { - jointEndLimb = GetClosestLimbOnRagdoll(PlayerInput.MousePosition, l => l != null && l != jointStartLimb && l.ActiveSprite != null); + jointEndLimb = GetClosestLimbOnRagdoll(PlayerInput.MousePosition, l => l != null && l != jointStartLimb && l.ActiveSprite != null && !l.Hidden); if (jointEndLimb != null && PlayerInput.PrimaryMouseButtonClicked()) { Vector2 anchor1 = anchor1Pos ?? Vector2.Zero; @@ -1105,7 +1102,7 @@ namespace Barotrauma.CharacterEditor } else if (PlayerInput.PrimaryMouseButtonClicked()) { - jointStartLimb = GetClosestLimbOnRagdoll(PlayerInput.MousePosition, l => selectedLimbs.Contains(l)); + jointStartLimb = GetClosestLimbOnRagdoll(PlayerInput.MousePosition, l => selectedLimbs.Contains(l) && !l.Hidden); anchor1Pos = ConvertUnits.ToDisplayUnits(jointStartLimb.body.FarseerBody.GetLocalPoint(ScreenToSim(PlayerInput.MousePosition))); jointCreationMode = JointCreationMode.Create; } @@ -1185,8 +1182,15 @@ namespace Barotrauma.CharacterEditor private void CreateLimb(XElement newElement) { - var lastLimbElement = RagdollParams.MainElement.Elements("limb").Last(); - lastLimbElement.AddAfterSelf(newElement); + var lastElement = RagdollParams.MainElement.GetChildElements("limb").LastOrDefault(); + if (lastElement != null) + { + lastElement.AddAfterSelf(newElement); + } + else + { + RagdollParams.MainElement.AddFirst(newElement); + } var newLimbParams = new RagdollParams.LimbParams(newElement, RagdollParams); RagdollParams.Limbs.Add(newLimbParams); character.AnimController.Recreate(); @@ -1217,12 +1221,7 @@ namespace Barotrauma.CharacterEditor new XAttribute("limb1anchor", $"{a1.X.Format(2)}, {a1.Y.Format(2)}"), new XAttribute("limb2anchor", $"{a2.X.Format(2)}, {a2.Y.Format(2)}") ); - var lastJointElement = RagdollParams.MainElement.Elements("joint").LastOrDefault(); - if (lastJointElement == null) - { - // If no joints exist, use the last limb element. - lastJointElement = RagdollParams.MainElement.Elements("limb").LastOrDefault(); - } + var lastJointElement = RagdollParams.MainElement.GetChildElements("joint").LastOrDefault() ?? RagdollParams.MainElement.GetChildElements("limb").LastOrDefault(); if (lastJointElement == null) { DebugConsole.ThrowError(GetCharacterEditorTranslation("CantAddJointsNoLimbElements")); @@ -2196,7 +2195,7 @@ namespace Barotrauma.CharacterEditor animTestPoseToggle = new GUITickBox(new RectTransform(toggleSize, layoutGroup.RectTransform), GetCharacterEditorTranslation("AnimationTestPose")) { Selected = character.AnimController.AnimationTestPose, - Enabled = character.IsHumanoid, + Enabled = true, OnSelected = box => { character.AnimController.AnimationTestPose = box.Selected; @@ -2760,7 +2759,7 @@ namespace Barotrauma.CharacterEditor return false; } #endif - if (!string.IsNullOrEmpty(RagdollParams.Texture) && !File.Exists(RagdollParams.Texture)) + if (!character.IsHuman && !string.IsNullOrEmpty(RagdollParams.Texture) && !File.Exists(RagdollParams.Texture)) { DebugConsole.ThrowError($"Invalid texture path: {RagdollParams.Texture}"); return false; @@ -3863,7 +3862,7 @@ namespace Barotrauma.CharacterEditor { // Head angle DrawRadialWidget(spriteBatch, SimToScreen(head.SimPosition), animParams.HeadAngle, GetCharacterEditorTranslation("HeadAngle"), Color.White, - angle => TryUpdateAnimParam("headangle", angle), circleRadius: 25, rotationOffset: collider.Rotation + MathHelper.Pi, clockWise: dir < 0, wrapAnglePi: true, holdPosition: true); + angle => TryUpdateAnimParam("headangle", angle), circleRadius: 25, rotationOffset: -collider.Rotation + head.Params.GetSpriteOrientation() * dir, clockWise: dir < 0, wrapAnglePi: true, holdPosition: true); // Head position and leaning Color color = GUI.Style.Red; if (animParams.IsGroundedAnimation) @@ -3972,7 +3971,7 @@ namespace Barotrauma.CharacterEditor } // Torso angle DrawRadialWidget(spriteBatch, SimToScreen(referencePoint), animParams.TorsoAngle, GetCharacterEditorTranslation("TorsoAngle"), Color.White, - angle => TryUpdateAnimParam("torsoangle", angle), rotationOffset: collider.Rotation + MathHelper.Pi, clockWise: dir < 0, wrapAnglePi: true, holdPosition: true); + angle => TryUpdateAnimParam("torsoangle", angle), rotationOffset: -collider.Rotation + torso.Params.GetSpriteOrientation() * dir, clockWise: dir < 0, wrapAnglePi: true, holdPosition: true); Color color = Color.DodgerBlue; if (animParams.IsGroundedAnimation) { @@ -4075,7 +4074,7 @@ namespace Barotrauma.CharacterEditor if (tail != null && fishParams != null) { DrawRadialWidget(spriteBatch, SimToScreen(tail.SimPosition), fishParams.TailAngle, GetCharacterEditorTranslation("TailAngle"), Color.White, - angle => TryUpdateAnimParam("tailangle", angle), circleRadius: 25, rotationOffset: collider.Rotation + MathHelper.Pi, clockWise: dir < 0, wrapAnglePi: true, holdPosition: true); + angle => TryUpdateAnimParam("tailangle", angle), circleRadius: 25, rotationOffset: -collider.Rotation + tail.Params.GetSpriteOrientation() * dir, clockWise: dir < 0, wrapAnglePi: true, holdPosition: true); } // Foot angle if (foot != null) @@ -4101,13 +4100,13 @@ namespace Barotrauma.CharacterEditor fishParams.FootAnglesInRadians[limb.Params.ID] = MathHelper.ToRadians(angle); TryUpdateAnimParam("footangles", fishParams.FootAngles); }, - circleRadius: 25, rotationOffset: collider.Rotation, clockWise: dir < 0, wrapAnglePi: true, autoFreeze: true); + circleRadius: 25, rotationOffset: -collider.Rotation + limb.Params.GetSpriteOrientation() * dir, clockWise: dir < 0, wrapAnglePi: true, autoFreeze: true); } } else if (humanParams != null) { DrawRadialWidget(spriteBatch, SimToScreen(foot.SimPosition), humanParams.FootAngle, GetCharacterEditorTranslation("FootAngle"), Color.White, - angle => TryUpdateAnimParam("footangle", angle), circleRadius: 25, rotationOffset: collider.Rotation + MathHelper.Pi, clockWise: dir < 0, wrapAnglePi: true); + angle => TryUpdateAnimParam("footangle", angle), circleRadius: 25, rotationOffset: -collider.Rotation + foot.Params.GetSpriteOrientation() * dir, clockWise: dir > 0, wrapAnglePi: true); } // Grounded only if (groundedParams != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs index 62fcc7ba7..fe25d54b9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs @@ -36,7 +36,7 @@ namespace Barotrauma private readonly GUITextBox seedBox; - private readonly GUITickBox lightingEnabled, cursorLightEnabled; + private readonly GUITickBox lightingEnabled, cursorLightEnabled, mirrorLevel; private Sprite editingSprite; @@ -74,9 +74,7 @@ namespace Barotrauma ruinParamsList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedLeftPanel.RectTransform)); ruinParamsList.OnSelected += (GUIComponent component, object obj) => { - var ruinGenerationParams = obj as RuinGenerationParams; - editorContainer.ClearChildren(); - new SerializableEntityEditor(editorContainer.Content.RectTransform, ruinGenerationParams, false, true, elementHeight: 20); + CreateOutpostGenerationParamsEditor(obj as OutpostGenerationParams); return true; }; @@ -95,101 +93,7 @@ namespace Barotrauma outpostParamsList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.2f), paddedLeftPanel.RectTransform)); outpostParamsList.OnSelected += (GUIComponent component, object obj) => { - var outpostGenerationParams = obj as OutpostGenerationParams; - editorContainer.ClearChildren(); - var outpostParamsEditor = new SerializableEntityEditor(editorContainer.Content.RectTransform, outpostGenerationParams, false, true, elementHeight: 20); - - // location type ------------------------- - - var locationTypeGroup = new GUILayoutGroup(new RectTransform(new Point(editorContainer.Content.Rect.Width, 20)), isHorizontal: true, childAnchor: Anchor.CenterLeft) - { - Stretch = true - }; - - new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), locationTypeGroup.RectTransform), TextManager.Get("outpostmoduleallowedlocationtypes"), textAlignment: Alignment.CenterLeft); - HashSet availableLocationTypes = new HashSet { "any" }; - foreach (LocationType locationType in LocationType.List) { availableLocationTypes.Add(locationType.Identifier); } - - var locationTypeDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), locationTypeGroup.RectTransform), - text: string.Join(", ", outpostGenerationParams.AllowedLocationTypes.Select(lt => TextManager.Capitalize(lt)) ?? "any".ToEnumerable()), selectMultiple: true); - foreach (string locationType in availableLocationTypes) - { - locationTypeDropDown.AddItem(TextManager.Capitalize(locationType), locationType); - if (outpostGenerationParams.AllowedLocationTypes.Contains(locationType)) - { - locationTypeDropDown.SelectItem(locationType); - } - } - if (!outpostGenerationParams.AllowedLocationTypes.Any()) - { - locationTypeDropDown.SelectItem("any"); - } - - locationTypeDropDown.OnSelected += (_, __) => - { - outpostGenerationParams.SetAllowedLocationTypes(locationTypeDropDown.SelectedDataMultiple.Cast()); - locationTypeDropDown.Text = ToolBox.LimitString(locationTypeDropDown.Text, locationTypeDropDown.Font, locationTypeDropDown.Rect.Width); - return true; - }; - locationTypeGroup.RectTransform.MinSize = new Point(locationTypeGroup.Rect.Width, locationTypeGroup.RectTransform.Children.Max(c => c.MinSize.Y)); - - outpostParamsEditor.AddCustomContent(locationTypeGroup, 100); - - // module count ------------------------- - - var moduleLabel = new GUITextBlock(new RectTransform(new Point(editorContainer.Content.Rect.Width, (int)(70 * GUI.Scale))), TextManager.Get("submarinetype.outpostmodules"), font: GUI.SubHeadingFont); - outpostParamsEditor.AddCustomContent(moduleLabel, 100); - - foreach (KeyValuePair moduleCount in outpostGenerationParams.ModuleCounts) - { - var moduleCountGroup = new GUILayoutGroup(new RectTransform(new Point(editorContainer.Content.Rect.Width, (int)(25 * GUI.Scale))), isHorizontal: true, childAnchor: Anchor.CenterLeft); - new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), moduleCountGroup.RectTransform), TextManager.Capitalize(moduleCount.Key), textAlignment: Alignment.CenterLeft); - new GUINumberInput(new RectTransform(new Vector2(0.5f, 1f), moduleCountGroup.RectTransform), GUINumberInput.NumberType.Int) - { - MinValueInt = 0, - MaxValueInt = 100, - IntValue = moduleCount.Value, - OnValueChanged = (numInput) => - { - outpostGenerationParams.SetModuleCount(moduleCount.Key, numInput.IntValue); - if (numInput.IntValue == 0) - { - outpostParamsList.Select(outpostParamsList.SelectedData); - } - } - }; - moduleCountGroup.RectTransform.MinSize = new Point(moduleCountGroup.Rect.Width, moduleCountGroup.RectTransform.Children.Max(c => c.MinSize.Y)); - outpostParamsEditor.AddCustomContent(moduleCountGroup, 100); - } - - // add module count ------------------------- - - var addModuleCountGroup = new GUILayoutGroup(new RectTransform(new Point(editorContainer.Content.Rect.Width, (int)(40 * GUI.Scale))), isHorizontal: true, childAnchor: Anchor.Center); - - HashSet availableFlags = new HashSet(); - foreach (string flag in OutpostGenerationParams.Params.SelectMany(p => p.ModuleCounts.Select(m => m.Key))) { availableFlags.Add(flag); } - foreach (var sub in SubmarineInfo.SavedSubmarines) - { - if (sub.OutpostModuleInfo == null) { continue; } - foreach (string flag in sub.OutpostModuleInfo.ModuleFlags) { availableFlags.Add(flag); } - } - - var moduleTypeDropDown = new GUIDropDown(new RectTransform(new Vector2(0.8f, 0.8f), addModuleCountGroup.RectTransform), - text: TextManager.Get("leveleditor.addmoduletype")); - foreach (string flag in availableFlags) - { - if (outpostGenerationParams.ModuleCounts.Any(mc => mc.Key.Equals(flag, StringComparison.OrdinalIgnoreCase))) { continue; } - moduleTypeDropDown.AddItem(TextManager.Capitalize(flag), flag); - } - moduleTypeDropDown.OnSelected += (_, userdata) => - { - outpostGenerationParams.SetModuleCount(userdata as string, 1); - outpostParamsList.Select(outpostParamsList.SelectedData); - return true; - }; - addModuleCountGroup.RectTransform.MinSize = new Point(addModuleCountGroup.Rect.Width, addModuleCountGroup.RectTransform.Children.Max(c => c.MinSize.Y)); - outpostParamsEditor.AddCustomContent(addModuleCountGroup, 100); - + CreateOutpostGenerationParamsEditor(obj as OutpostGenerationParams); return true; }; @@ -239,10 +143,12 @@ namespace Barotrauma editorContainer = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), paddedRightPanel.RectTransform)); - var seedContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), paddedRightPanel.RectTransform), isHorizontal: true); + var seedContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.02f), paddedRightPanel.RectTransform), isHorizontal: true); new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), seedContainer.RectTransform), TextManager.Get("leveleditor.levelseed")); seedBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), seedContainer.RectTransform), ToolBox.RandomSeed(8)); + mirrorLevel = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.02f), paddedRightPanel.RectTransform), TextManager.Get("mirrorentityx")); + new GUIButton(new RectTransform(new Vector2(1.0f, 0.05f), paddedRightPanel.RectTransform), TextManager.Get("leveleditor.generate")) { @@ -253,7 +159,7 @@ namespace Barotrauma GameMain.LightManager.ClearLights(); LevelData levelData = LevelData.CreateRandom(seedBox.Text, generationParams: selectedParams); levelData.ForceOutpostGenerationParams = outpostParamsList.SelectedData as OutpostGenerationParams; - Level.Generate(levelData, mirror: false); + Level.Generate(levelData, mirror: mirrorLevel.Selected); GameMain.LightManager.AddLight(pointerLightSource); if (!wasLevelLoaded || cam.Position.X < 0 || cam.Position.Y < 0 || cam.Position.Y > Level.Loaded.Size.X || cam.Position.Y > Level.Loaded.Size.Y) { @@ -408,7 +314,7 @@ namespace Barotrauma editorContainer.ClearChildren(); ruinParamsList.Content.ClearChildren(); - foreach (RuinGenerationParams genParams in RuinGenerationParams.List) + foreach (RuinGenerationParams genParams in RuinGenerationParams.RuinParams) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), ruinParamsList.Content.RectTransform) { MinSize = new Point(0, 20) }, genParams.Name) @@ -500,6 +406,104 @@ namespace Barotrauma } } + private void CreateOutpostGenerationParamsEditor(OutpostGenerationParams outpostGenerationParams) + { + editorContainer.ClearChildren(); + var outpostParamsEditor = new SerializableEntityEditor(editorContainer.Content.RectTransform, outpostGenerationParams, false, true, elementHeight: 20); + + // location type ------------------------- + + var locationTypeGroup = new GUILayoutGroup(new RectTransform(new Point(editorContainer.Content.Rect.Width, 20)), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = true + }; + + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), locationTypeGroup.RectTransform), TextManager.Get("outpostmoduleallowedlocationtypes"), textAlignment: Alignment.CenterLeft); + HashSet availableLocationTypes = new HashSet { "any" }; + foreach (LocationType locationType in LocationType.List) { availableLocationTypes.Add(locationType.Identifier); } + + var locationTypeDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), locationTypeGroup.RectTransform), + text: string.Join(", ", outpostGenerationParams.AllowedLocationTypes.Select(lt => TextManager.Capitalize(lt)) ?? "any".ToEnumerable()), selectMultiple: true); + foreach (string locationType in availableLocationTypes) + { + locationTypeDropDown.AddItem(TextManager.Capitalize(locationType), locationType); + if (outpostGenerationParams.AllowedLocationTypes.Contains(locationType)) + { + locationTypeDropDown.SelectItem(locationType); + } + } + if (!outpostGenerationParams.AllowedLocationTypes.Any()) + { + locationTypeDropDown.SelectItem("any"); + } + + locationTypeDropDown.OnSelected += (_, __) => + { + outpostGenerationParams.SetAllowedLocationTypes(locationTypeDropDown.SelectedDataMultiple.Cast()); + locationTypeDropDown.Text = ToolBox.LimitString(locationTypeDropDown.Text, locationTypeDropDown.Font, locationTypeDropDown.Rect.Width); + return true; + }; + locationTypeGroup.RectTransform.MinSize = new Point(locationTypeGroup.Rect.Width, locationTypeGroup.RectTransform.Children.Max(c => c.MinSize.Y)); + + outpostParamsEditor.AddCustomContent(locationTypeGroup, 100); + + // module count ------------------------- + + var moduleLabel = new GUITextBlock(new RectTransform(new Point(editorContainer.Content.Rect.Width, (int)(70 * GUI.Scale))), TextManager.Get("submarinetype.outpostmodules"), font: GUI.SubHeadingFont); + outpostParamsEditor.AddCustomContent(moduleLabel, 100); + + foreach (KeyValuePair moduleCount in outpostGenerationParams.ModuleCounts) + { + var moduleCountGroup = new GUILayoutGroup(new RectTransform(new Point(editorContainer.Content.Rect.Width, (int)(25 * GUI.Scale))), isHorizontal: true, childAnchor: Anchor.CenterLeft); + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), moduleCountGroup.RectTransform), TextManager.Capitalize(moduleCount.Key), textAlignment: Alignment.CenterLeft); + new GUINumberInput(new RectTransform(new Vector2(0.5f, 1f), moduleCountGroup.RectTransform), GUINumberInput.NumberType.Int) + { + MinValueInt = 0, + MaxValueInt = 100, + IntValue = moduleCount.Value, + OnValueChanged = (numInput) => + { + outpostGenerationParams.SetModuleCount(moduleCount.Key, numInput.IntValue); + if (numInput.IntValue == 0) + { + outpostParamsList.Select(outpostParamsList.SelectedData); + } + } + }; + moduleCountGroup.RectTransform.MinSize = new Point(moduleCountGroup.Rect.Width, moduleCountGroup.RectTransform.Children.Max(c => c.MinSize.Y)); + outpostParamsEditor.AddCustomContent(moduleCountGroup, 100); + } + + // add module count ------------------------- + + var addModuleCountGroup = new GUILayoutGroup(new RectTransform(new Point(editorContainer.Content.Rect.Width, (int)(40 * GUI.Scale))), isHorizontal: true, childAnchor: Anchor.Center); + + HashSet availableFlags = new HashSet(); + foreach (string flag in OutpostGenerationParams.Params.SelectMany(p => p.ModuleCounts.Select(m => m.Key))) { availableFlags.Add(flag); } + foreach (var sub in SubmarineInfo.SavedSubmarines) + { + if (sub.OutpostModuleInfo == null) { continue; } + foreach (string flag in sub.OutpostModuleInfo.ModuleFlags) { availableFlags.Add(flag); } + } + + var moduleTypeDropDown = new GUIDropDown(new RectTransform(new Vector2(0.8f, 0.8f), addModuleCountGroup.RectTransform), + text: TextManager.Get("leveleditor.addmoduletype")); + foreach (string flag in availableFlags) + { + if (outpostGenerationParams.ModuleCounts.Any(mc => mc.Key.Equals(flag, StringComparison.OrdinalIgnoreCase))) { continue; } + moduleTypeDropDown.AddItem(TextManager.Capitalize(flag), flag); + } + moduleTypeDropDown.OnSelected += (_, userdata) => + { + outpostGenerationParams.SetModuleCount(userdata as string, 1); + outpostParamsList.Select(outpostParamsList.SelectedData); + return true; + }; + addModuleCountGroup.RectTransform.MinSize = new Point(addModuleCountGroup.Rect.Width, addModuleCountGroup.RectTransform.Children.Max(c => c.MinSize.Y)); + outpostParamsEditor.AddCustomContent(addModuleCountGroup, 100); + + } + private void CreateLevelObjectEditor(LevelObjectPrefab levelObjectPrefab) { editorContainer.ClearChildren(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index 6c108709b..2a12028a9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -435,7 +435,7 @@ namespace Barotrauma menuTabs[(int)Tab.Settings] = new GUIFrame(new RectTransform(new Vector2(relativeSize.X, 0.8f), GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeSpacing }, style: null); - menuTabs[(int)Tab.NewGame] = new GUIFrame(new RectTransform(relativeSize, GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeSpacing }); + menuTabs[(int)Tab.NewGame] = new GUIFrame(new RectTransform(relativeSize * new Vector2(1.0f, 1.15f), GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeSpacing }); menuTabs[(int)Tab.LoadGame] = new GUIFrame(new RectTransform(relativeSize, GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeSpacing }); CreateCampaignSetupUI(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index 62d6c6aad..093e093db 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -982,22 +982,24 @@ namespace Barotrauma Visible = false, CanBeFocused = false }; - continue; } - missionTypeTickBoxes[index] = new GUITickBox(new RectTransform(Vector2.One, frame.RectTransform), - TextManager.Get("MissionType." + missionType.ToString())) + else { - UserData = (int)missionType, - ToolTip = TextManager.Get("MissionTypeDescription." + missionType.ToString(), returnNull: true), - OnSelected = (tickbox) => + missionTypeTickBoxes[index] = new GUITickBox(new RectTransform(Vector2.One, frame.RectTransform), + TextManager.Get("MissionType." + missionType.ToString())) { - int missionTypeOr = tickbox.Selected ? (int)tickbox.UserData : (int)MissionType.None; - int missionTypeAnd = (int)MissionType.All & (!tickbox.Selected ? (~(int)tickbox.UserData) : (int)MissionType.All); - GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, (int)missionTypeOr, (int)missionTypeAnd); - return true; - } - }; - frame.RectTransform.MinSize = missionTypeTickBoxes[index].RectTransform.MinSize; + UserData = (int)missionType, + ToolTip = TextManager.Get("MissionTypeDescription." + missionType.ToString(), returnNull: true), + OnSelected = (tickbox) => + { + int missionTypeOr = tickbox.Selected ? (int)tickbox.UserData : (int)MissionType.None; + int missionTypeAnd = (int)MissionType.All & (!tickbox.Selected ? (~(int)tickbox.UserData) : (int)MissionType.All); + GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, (int)missionTypeOr, (int)missionTypeAnd); + return true; + } + }; + frame.RectTransform.MinSize = missionTypeTickBoxes[index].RectTransform.MinSize; + } index++; } clientDisabledElements.AddRange(missionTypeTickBoxes); @@ -1428,9 +1430,9 @@ namespace Barotrauma bool isGameRunning = GameMain.GameSession?.IsRunning ?? false; - infoContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, isGameRunning ? 0.95f : 0.9f), parent.RectTransform, Anchor.BottomCenter), childAnchor: Anchor.TopCenter) + infoContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, isGameRunning ? 0.97f : 0.92f), parent.RectTransform, Anchor.BottomCenter), childAnchor: Anchor.TopCenter) { - RelativeSpacing = 0.015f, + RelativeSpacing = 0.0f, Stretch = true, UserData = characterInfo }; @@ -1486,21 +1488,24 @@ namespace Barotrauma } }; + //spacing + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.006f), infoContainer.RectTransform), style: null); + if (allowEditing) { - GUILayoutGroup characterInfoTabs = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.02f), infoContainer.RectTransform), isHorizontal: true) + GUILayoutGroup characterInfoTabs = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.016f), infoContainer.RectTransform), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.02f }; - jobPreferencesButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.33f), characterInfoTabs.RectTransform), + jobPreferencesButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1f), characterInfoTabs.RectTransform), TextManager.Get("JobPreferences"), style: "GUITabButton") { Selected = true, OnClicked = SelectJobPreferencesTab }; - appearanceButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.33f), characterInfoTabs.RectTransform), + appearanceButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1f), characterInfoTabs.RectTransform), TextManager.Get("CharacterAppearance"), style: "GUITabButton") { OnClicked = SelectAppearanceTab @@ -1515,7 +1520,7 @@ namespace Barotrauma JobPreferenceContainer = new GUIFrame(new RectTransform(Vector2.One, characterInfoFrame.RectTransform), style: "GUIFrameListBox"); - characterInfo.CreateIcon(new RectTransform(new Vector2(1.0f, 0.4f), JobPreferenceContainer.RectTransform, Anchor.TopCenter)); + characterInfo.CreateIcon(new RectTransform(new Vector2(1.0f, 0.4f), JobPreferenceContainer.RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0f, 0.025f) }); JobList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.6f), JobPreferenceContainer.RectTransform, Anchor.BottomCenter), true) { Enabled = true, @@ -3191,7 +3196,7 @@ namespace Barotrauma { GUICustomComponent characterIcon = JobPreferenceContainer.GetChild(); JobPreferenceContainer.RemoveChild(characterIcon); - GameMain.Client.CharacterInfo.CreateIcon(new RectTransform(new Vector2(1.0f, 0.4f), JobPreferenceContainer.RectTransform, Anchor.TopCenter)); + GameMain.Client.CharacterInfo.CreateIcon(new RectTransform(new Vector2(1.0f, 0.4f), JobPreferenceContainer.RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.025f) }); GUIListBox listBox = JobPreferenceContainer.GetChild(); /*foreach (Sprite sprite in jobPreferenceSprites) { sprite.Remove(); } @@ -3297,10 +3302,10 @@ namespace Barotrauma private GUIButton CreateJobVariantButton(Pair jobPrefab, int variantIndex, int variantCount, GUIComponent slot) { - float relativeSize = 0.2f; + float relativeSize = 0.15f; var btn = new GUIButton(new RectTransform(new Vector2(relativeSize), slot.RectTransform, Anchor.TopCenter, scaleBasis: ScaleBasis.BothHeight) - { RelativeOffset = new Vector2(relativeSize * 1.05f * (variantIndex - (variantCount - 1) / 2.0f), 0.02f) }, + { RelativeOffset = new Vector2(relativeSize * 1.3f * (variantIndex - (variantCount - 1) / 2.0f), 0.02f) }, (variantIndex + 1).ToString(), style: "JobVariantButton") { Selected = jobPrefab.Second == variantIndex, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 5b4481583..aaad0e557 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -21,7 +21,7 @@ namespace Barotrauma { class SubEditorScreen : EditorScreen { - private static readonly string[] crewExperienceLevels = + private static readonly string[] crewExperienceLevels = { "CrewExperienceLow", "CrewExperienceMid", @@ -45,7 +45,7 @@ namespace Barotrauma NonLinkedGaps, TooManyLights } - + public static Vector2 MouseDragStart = Vector2.Zero; private readonly Point defaultPreviewImageSize = new Point(640, 368); @@ -100,6 +100,7 @@ namespace Barotrauma private GUIListBox previouslyUsedList; private GUIFrame undoBufferPanel; + private GUIFrame undoBufferDisclaimer; private GUIListBox undoBufferList; private GUIDropDown linkedSubBox; @@ -138,13 +139,13 @@ namespace Barotrauma //a Character used for picking up and manipulating items private Character dummyCharacter; - + /// /// Prefab used for dragging from the item catalog into inventories /// /// public static MapEntityPrefab DraggedItemPrefab; - + /// /// Currently opened hand-held item container like crates /// @@ -296,7 +297,7 @@ namespace Barotrauma }; new GUIFrame(new RectTransform(new Vector2(0.01f, 0.9f), paddedTopPanel.RectTransform), style: "VerticalLine"); - + new GUIButton(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "SaveButton") { ToolTip = TextManager.Get("SaveSubButton") + "‖color:125,125,125‖\nCtrl + S‖color:end‖", @@ -478,7 +479,10 @@ namespace Barotrauma { Visible = false }; - undoBufferList = new GUIListBox(new RectTransform(new Vector2(0.925f, 0.9f), undoBufferPanel.RectTransform, Anchor.Center)) + + Vector2 undoSize = new Vector2(0.925f, 0.9f); + + undoBufferList = new GUIListBox(new RectTransform(undoSize, undoBufferPanel.RectTransform, Anchor.Center)) { ScrollBarVisible = true, OnSelected = (_, userData) => @@ -504,11 +508,21 @@ namespace Barotrauma { Undo(amount - 1); } - + return true; } }; - + + undoBufferDisclaimer = new GUIFrame(new RectTransform(undoSize, undoBufferPanel.RectTransform, Anchor.Center), style: null) + { + Color = Color.Black, + Visible = false + }; + new GUITextBlock(new RectTransform(Vector2.One, undoBufferDisclaimer.RectTransform, Anchor.Center), text: TextManager.Get("editor.undounavailable"), textAlignment: Alignment.Center, wrap: true, font: GUI.SubHeadingFont) + { + TextColor = GUI.Style.Orange + }; + UpdateUndoHistoryPanel(); //----------------------------------------------- @@ -516,9 +530,9 @@ namespace Barotrauma showEntitiesPanel = new GUIFrame(new RectTransform(new Vector2(0.1f, 0.5f), GUI.Canvas) { MinSize = new Point(190, 0) - }) - { - Visible = false + }) + { + Visible = false }; GUILayoutGroup paddedShowEntitiesPanel = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.98f), showEntitiesPanel.RectTransform, Anchor.Center)) @@ -599,14 +613,14 @@ namespace Barotrauma List availableSubcategories = new List(); foreach (var prefab in MapEntityPrefab.List) { - if (!string.IsNullOrEmpty(prefab.Subcategory) && !availableSubcategories.Contains(prefab.Subcategory)) - { - availableSubcategories.Add(prefab.Subcategory); + if (!string.IsNullOrEmpty(prefab.Subcategory) && !availableSubcategories.Contains(prefab.Subcategory)) + { + availableSubcategories.Add(prefab.Subcategory); } } foreach (string subcategory in availableSubcategories) { - var tb = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.1f), subcategoryList.Content.RectTransform), + var tb = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.1f), subcategoryList.Content.RectTransform), TextManager.Get("subcategory." + subcategory, returnNull: true) ?? subcategory, font: GUI.SmallFont) { UserData = subcategory, @@ -643,7 +657,7 @@ namespace Barotrauma AbsoluteSpacing = (int)(GUI.Scale * 4) }; - var itemCountText = new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.0f), paddedEntityCountPanel.RectTransform), TextManager.Get("Items"), + var itemCountText = new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.0f), paddedEntityCountPanel.RectTransform), TextManager.Get("Items"), textAlignment: Alignment.CenterLeft, font: GUI.SmallFont); var itemCount = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1.0f), itemCountText.RectTransform, Anchor.TopRight, Pivot.TopLeft), "", textAlignment: Alignment.CenterRight); itemCount.TextGetter = () => @@ -652,7 +666,7 @@ namespace Barotrauma return Item.ItemList.Count.ToString(); }; - var structureCountText = new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.0f), paddedEntityCountPanel.RectTransform), TextManager.Get("Structures"), + var structureCountText = new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.0f), paddedEntityCountPanel.RectTransform), TextManager.Get("Structures"), textAlignment: Alignment.CenterLeft, font: GUI.SmallFont); var structureCount = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1.0f), structureCountText.RectTransform, Anchor.TopRight, Pivot.TopLeft), "", textAlignment: Alignment.CenterRight); structureCount.TextGetter = () => @@ -662,7 +676,7 @@ namespace Barotrauma return count.ToString(); }; - var wallCountText = new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.0f), paddedEntityCountPanel.RectTransform), TextManager.Get("Walls"), + var wallCountText = new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.0f), paddedEntityCountPanel.RectTransform), TextManager.Get("Walls"), textAlignment: Alignment.CenterLeft, font: GUI.SmallFont); var wallCount = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1.0f), wallCountText.RectTransform, Anchor.TopRight, Pivot.TopLeft), "", textAlignment: Alignment.CenterRight); wallCount.TextGetter = () => @@ -670,8 +684,8 @@ namespace Barotrauma wallCount.TextColor = ToolBox.GradientLerp(Structure.WallList.Count / 500.0f, GUI.Style.Green, GUI.Style.Orange, GUI.Style.Red); return Structure.WallList.Count.ToString(); }; - - var lightCountLabel = 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 lightCountText = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1.0f), lightCountLabel.RectTransform, Anchor.TopRight, Pivot.TopLeft), "", textAlignment: Alignment.CenterRight); lightCountText.TextGetter = () => @@ -685,7 +699,7 @@ namespace Barotrauma lightCountText.TextColor = ToolBox.GradientLerp(lightCount / 250.0f, GUI.Style.Green, GUI.Style.Orange, GUI.Style.Red); return lightCount.ToString(); }; - var shadowCastingLightCountLabel = 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 shadowCastingLightCountText = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1.0f), shadowCastingLightCountLabel.RectTransform, Anchor.TopRight, Pivot.TopLeft), "", textAlignment: Alignment.CenterRight); shadowCastingLightCountText.TextGetter = () => @@ -780,7 +794,7 @@ namespace Barotrauma { if (text == lastFilter) { return true; } lastFilter = text; - FilterEntities(text); + FilterEntities(text); return true; }; @@ -794,7 +808,7 @@ namespace Barotrauma OnClicked = (btn, userdata) => { OpenEntityMenu(null); - return true; + return true; } }); @@ -962,7 +976,7 @@ namespace Barotrauma #if !DEBUG if (ep.HideInMenus) { continue; } #endif - CreateEntityElement(ep, entitiesPerRow, entityListInner.Content); + CreateEntityElement(ep, entitiesPerRow, entityListInner.Content); } entityListInner.UpdateScrollBarSize(); @@ -972,7 +986,7 @@ namespace Barotrauma entityListInner.RectTransform.NonScaledSize = new Point(entityListInner.Rect.Width, contentHeight); entityListInner.RectTransform.MinSize = new Point(0, contentHeight); - entityListInner.Content.RectTransform.SortChildren((i1, i2) => + entityListInner.Content.RectTransform.SortChildren((i1, i2) => string.Compare(((MapEntityPrefab)i1.GUIComponent.UserData). Name, (i2.GUIComponent.UserData as MapEntityPrefab)?.Name, StringComparison.Ordinal)); } @@ -1153,8 +1167,8 @@ namespace Barotrauma { AutoSaveInfo = XMLExtensions.TryLoadXml(autoSaveInfoPath); } - - GameMain.LightManager.AmbientLight = + + GameMain.LightManager.AmbientLight = Level.Loaded?.GenerationParams?.AmbientLightColor ?? new Color(3, 3, 3, 3); @@ -1219,9 +1233,9 @@ namespace Barotrauma { CoroutineManager.StartCoroutine(AutoSaveCoroutine(), "SubEditorAutoSave"); } - + ImageManager.OnEditorSelected(); - + GameAnalyticsManager.SetCustomDimension01("editor"); if (!GameMain.Config.EditorDisclaimerShown) { @@ -1293,10 +1307,10 @@ namespace Barotrauma tempTarget = DateTime.Now; wasPaused = true; } - + if (!GameMain.Instance.Paused && wasPaused) { - wasPaused = false; + wasPaused = false; target = target.AddSeconds((DateTime.Now - tempTarget).TotalSeconds); } yield return CoroutineStatus.Running; @@ -1309,7 +1323,7 @@ namespace Barotrauma } yield return CoroutineStatus.Success; } - + public override void Deselect() { base.Deselect(); @@ -1350,7 +1364,7 @@ namespace Barotrauma dummyCharacter = null; GameMain.World.ProcessChanges(); } - + GUIMessageBox.MessageBoxes.ForEachMod(component => { if (component is GUIMessageBox { Closed: false, UserData: "colorpicker" } msgBox) @@ -1363,7 +1377,7 @@ namespace Barotrauma msgBox.Close(); } }); - + ClearFilter(); } @@ -1429,8 +1443,8 @@ namespace Barotrauma min?.Remove(); } - XElement newElement = new XElement("AutoSave", - new XAttribute("file", filePath), + XElement newElement = new XElement("AutoSave", + new XAttribute("file", filePath), new XAttribute("name", Submarine.MainSub.Info.Name), new XAttribute("time", (ulong)time.TotalSeconds)); AutoSaveInfo.Root.Add(newElement); @@ -1444,7 +1458,7 @@ namespace Barotrauma DebugConsole.ThrowError("Saving auto save info to \"" + autoSaveInfoPath + "\" failed!", e); } }); - + Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false; CrossThread.RequestExecutionOnMainThread(DisplayAutoSavePrompt); } @@ -1522,11 +1536,16 @@ namespace Barotrauma { #if DEBUG var existingFiles = ContentPackage.GetFilesOfType(GameMain.VanillaContent.ToEnumerable(), contentType); + if (contentType == ContentType.OutpostModule) + { + existingFiles = existingFiles.Where(f => f.Path.Contains("Ruin") == Submarine.MainSub.Info.OutpostModuleInfo.ModuleFlags.Contains("ruin")); + } #else var existingFiles = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages.Where(c => c != GameMain.VanillaContent), contentType); #endif - specialSavePath = existingFiles.FirstOrDefault(f => + specialSavePath = existingFiles.FirstOrDefault(f => Path.GetFullPath(f.Path) != Path.GetFullPath(SubmarineInfo.SavePath) && ContentPackage.IsModFilePathAllowed(f.Path))?.Path; + if (!string.IsNullOrEmpty(specialSavePath)) { specialSavePath = Path.GetDirectoryName(specialSavePath); @@ -1569,7 +1588,7 @@ namespace Barotrauma saveFrame = null; msgBox.Close(); return true; - }; + }; msgBox.Buttons[1].OnClicked = (bt, userdata) => { SaveSubToFile(nameBox.Text); @@ -1592,7 +1611,7 @@ namespace Barotrauma GUI.AddMessage(TextManager.Get("SubNameMissingWarning"), GUI.Style.Red); return false; } - + foreach (var illegalChar in Path.GetInvalidFileNameChars()) { if (!name.Contains(illegalChar)) continue; @@ -1602,7 +1621,7 @@ namespace Barotrauma string savePath = name + ".sub"; string prevSavePath = null; - string directoryName = Submarine.MainSub?.Info?.FilePath == null ? + string directoryName = Submarine.MainSub?.Info?.FilePath == null ? SubmarineInfo.SavePath : Path.GetDirectoryName(Submarine.MainSub.Info.FilePath); if (!string.IsNullOrEmpty(specialSavePath)) { @@ -1721,7 +1740,7 @@ namespace Barotrauma Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false; Submarine.MainSub.CheckForErrors(); - + GUI.AddMessage(TextManager.GetWithVariable("SubSavedNotification", "[filepath]", savePath), GUI.Style.Green); SubmarineInfo.RefreshSavedSub(savePath); @@ -1729,11 +1748,11 @@ namespace Barotrauma string downloadFolder = Path.GetFullPath(SaveUtil.SubmarineDownloadFolder); linkedSubBox.ClearChildren(); - foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines) - { + foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines) + { if (sub.Type != SubmarineType.Player) { continue; } if (Path.GetDirectoryName(Path.GetFullPath(sub.FilePath)) == downloadFolder) { continue; } - linkedSubBox.AddItem(sub.Name, sub); + linkedSubBox.AddItem(sub.Name, sub); } subNameLabel.Text = ToolBox.LimitString(Submarine.MainSub.Info.Name, subNameLabel.Font, subNameLabel.Rect.Width); } @@ -1744,7 +1763,7 @@ namespace Barotrauma private void CreateSaveScreen(bool quickSave = false) { if (saveFrame != null) { return; } - + if (!quickSave) { CloseItem(); @@ -1757,7 +1776,7 @@ namespace Barotrauma }; new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, saveFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); - + var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.55f, 0.6f), saveFrame.RectTransform, Anchor.Center) { MinSize = new Point(750, 500) }); var paddedSaveFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), innerFrame.RectTransform, Anchor.Center)) { Stretch = true, RelativeSpacing = 0.02f }; @@ -1767,7 +1786,7 @@ namespace Barotrauma var leftColumn = new GUILayoutGroup(new RectTransform(new Vector2(0.55f, 1.0f), columnArea.RectTransform)) { RelativeSpacing = 0.01f, Stretch = true }; var rightColumn = new GUILayoutGroup(new RectTransform(new Vector2(0.42f, 1.0f), columnArea.RectTransform)) { RelativeSpacing = 0.02f, Stretch = true }; - // left column ----------------------------------------------------------------------- + // left column ----------------------------------------------------------------------- var nameHeaderGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.03f), leftColumn.RectTransform), true); var saveSubLabel = new GUITextBlock(new RectTransform(new Vector2(.5f, 1f), nameHeaderGroup.RectTransform), @@ -1837,6 +1856,7 @@ namespace Barotrauma subTypeContainer.RectTransform.MinSize = new Point(0, subTypeContainer.RectTransform.Children.Max(c => c.MinSize.Y)); foreach (SubmarineType subType in Enum.GetValues(typeof(SubmarineType))) { + if (subType == SubmarineType.Ruin) { continue; } string textTag = "SubmarineType." + subType; if (subType == SubmarineType.EnemySubmarine && !TextManager.ContainsTag(textTag)) { @@ -1866,13 +1886,14 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), outpostModuleGroup.RectTransform), TextManager.Get("outpostmoduletype"), textAlignment: Alignment.CenterLeft); HashSet availableFlags = new HashSet(); foreach (string flag in OutpostGenerationParams.Params.SelectMany(p => p.ModuleCounts.Select(m => m.Key))) { availableFlags.Add(flag); } + foreach (string flag in RuinGeneration.RuinGenerationParams.RuinParams.SelectMany(p => p.ModuleCounts.Select(m => m.Key))) { availableFlags.Add(flag); } foreach (var sub in SubmarineInfo.SavedSubmarines) { if (sub.OutpostModuleInfo == null) { continue; } - foreach (string flag in sub.OutpostModuleInfo.ModuleFlags) + foreach (string flag in sub.OutpostModuleInfo.ModuleFlags) { if (flag == "none") { continue; } - availableFlags.Add(flag); + availableFlags.Add(flag); } } @@ -1881,7 +1902,7 @@ namespace Barotrauma foreach (string flag in availableFlags) { moduleTypeDropDown.AddItem(TextManager.Capitalize(flag), flag); - if (Submarine.MainSub?.Info?.OutpostModuleInfo == null) { continue; } + if (Submarine.MainSub?.Info?.OutpostModuleInfo == null) { continue; } if (Submarine.MainSub.Info.OutpostModuleInfo.ModuleFlags.Contains(flag)) { moduleTypeDropDown.SelectItem(flag); @@ -1890,9 +1911,9 @@ namespace Barotrauma moduleTypeDropDown.OnSelected += (_, __) => { if (Submarine.MainSub?.Info?.OutpostModuleInfo == null) { return false; } - Submarine.MainSub.Info.OutpostModuleInfo.SetFlags(moduleTypeDropDown.SelectedDataMultiple.Cast()); + Submarine.MainSub.Info.OutpostModuleInfo.SetFlags(moduleTypeDropDown.SelectedDataMultiple.Cast()); moduleTypeDropDown.Text = ToolBox.LimitString( - Submarine.MainSub.Info.OutpostModuleInfo.ModuleFlags.Any(f => f != "none") ? moduleTypeDropDown.Text : "None", + Submarine.MainSub.Info.OutpostModuleInfo.ModuleFlags.Any(f => f != "none") ? moduleTypeDropDown.Text : "None", moduleTypeDropDown.Font, moduleTypeDropDown.Rect.Width); return true; }; @@ -1903,11 +1924,11 @@ namespace Barotrauma var allowAttachGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.1f), outpostSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), allowAttachGroup.RectTransform), TextManager.Get("outpostmoduleallowattachto"), textAlignment: Alignment.CenterLeft); - + var allowAttachDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), allowAttachGroup.RectTransform), text: string.Join(", ", Submarine.MainSub?.Info?.OutpostModuleInfo?.AllowAttachToModules.Select(s => TextManager.Capitalize(s)) ?? "Any".ToEnumerable()), selectMultiple: true); allowAttachDropDown.AddItem(TextManager.Capitalize("any"), "any"); - if (Submarine.MainSub.Info.OutpostModuleInfo == null || + if (Submarine.MainSub.Info.OutpostModuleInfo == null || !Submarine.MainSub.Info.OutpostModuleInfo.AllowAttachToModules.Any() || Submarine.MainSub.Info.OutpostModuleInfo.AllowAttachToModules.All(s => s.Equals("any", StringComparison.OrdinalIgnoreCase))) { @@ -2069,7 +2090,7 @@ namespace Barotrauma new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), priceGroup.RectTransform), GUINumberInput.NumberType.Int, hidePlusMinusButtons: true) { IntValue = Math.Max(Submarine.MainSub?.Info?.Price ?? basePrice, basePrice), - MinValueInt = basePrice, + MinValueInt = basePrice, MaxValueInt = 999999, OnValueChanged = (numberInput) => { @@ -2204,7 +2225,7 @@ namespace Barotrauma // right column --------------------------------------------------- new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), rightColumn.RectTransform), TextManager.Get("SubPreviewImage"), font: GUI.SubHeadingFont); - + var previewImageHolder = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.5f), rightColumn.RectTransform), style: null) { Color = Color.Black, CanBeFocused = false }; previewImage = new GUIImage(new RectTransform(Vector2.One, previewImageHolder.RectTransform), Submarine.MainSub?.Info.PreviewImage, scaleToFit: true); @@ -2262,7 +2283,7 @@ namespace Barotrauma var settingsLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), horizontalArea.RectTransform), TextManager.Get("SaveSubDialogSettings"), wrap: true, font: GUI.SmallFont); - var tagContainer = new GUIListBox(new RectTransform(new Vector2(0.5f, 1.0f - settingsLabel.RectTransform.RelativeSize.Y), + var tagContainer = new GUIListBox(new RectTransform(new Vector2(0.5f, 1.0f - settingsLabel.RectTransform.RelativeSize.Y), horizontalArea.RectTransform, Anchor.BottomLeft), style: "InnerFrame"); @@ -2393,9 +2414,9 @@ namespace Barotrauma Stretch = true }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedSaveFrame.RectTransform), + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedSaveFrame.RectTransform), TextManager.Get("SaveItemAssemblyDialogHeader"), font: GUI.LargeFont); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedSaveFrame.RectTransform), + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedSaveFrame.RectTransform), TextManager.Get("SaveItemAssemblyDialogName")); nameBox = new GUITextBox(new RectTransform(new Vector2(0.6f, 0.1f), paddedSaveFrame.RectTransform)); @@ -2563,7 +2584,7 @@ namespace Barotrauma RelativeSpacing = 0.1f, Stretch = true }; - + var searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedLoadFrame.RectTransform), font: GUI.Font, createClearButton: true); var searchTitle = new GUITextBlock(new RectTransform(Vector2.One, searchBox.RectTransform), TextManager.Get("serverlog.filter"), textAlignment: Alignment.CenterLeft, font: GUI.Font) @@ -2659,7 +2680,7 @@ namespace Barotrauma deleteButton.Enabled = false; return true; }; - + if (AutoSaveInfo?.Root != null) { @@ -2679,11 +2700,11 @@ namespace Barotrauma DateTime time = DateTime.MinValue.AddSeconds(saveElement.GetAttributeUInt64("time", 0)); TimeSpan difference = DateTime.UtcNow - time; - string tooltip = TextManager.GetWithVariables("subeditor.autosaveage", + string tooltip = TextManager.GetWithVariables("subeditor.autosaveage", new[] { - "[hours]", - "[minutes]", + "[hours]", + "[minutes]", "[seconds]" }, new[] @@ -2701,7 +2722,7 @@ namespace Barotrauma if (totalMinutes < 1) { timeFormat = TextManager.Get("subeditor.savedjustnow"); - } + } else if (totalMinutes > 60) { timeFormat = TextManager.Get("subeditor.savedmorethanhour"); @@ -2710,7 +2731,7 @@ namespace Barotrauma { timeFormat = TextManager.GetWithVariable("subeditor.saveageminutes", "[minutes]", difference.Minutes.ToString()); } - + string entryName = TextManager.GetWithVariables("subeditor.autosaveentry", new []{ "[submarine]", "[saveage]" }, new []{ submarineName, timeFormat }); loadAutoSave.AddItem(entryName, saveElement, tooltip); @@ -2759,13 +2780,13 @@ namespace Barotrauma if (string.IsNullOrWhiteSpace(filePath)) { return; } var loadedSub = Submarine.Load(new SubmarineInfo(filePath), true); - + // set the submarine file path to the "default" value loadedSub.Info.FilePath = Path.Combine(SubmarineInfo.SavePath, $"{TextManager.Get("UnspecifiedSubFileName")}.sub"); loadedSub.Info.Name = TextManager.Get("UnspecifiedSubFileName"); - try + try { - loadedSub.Info.Name = loadedSub.Info.SubmarineElement.GetAttributeString("name", loadedSub.Info.Name); + loadedSub.Info.Name = loadedSub.Info.SubmarineElement.GetAttributeString("name", loadedSub.Info.Name); } catch (Exception e) { @@ -2776,9 +2797,9 @@ namespace Barotrauma Submarine.MainSub.UpdateTransform(); Submarine.MainSub.Info.Name = loadedSub.Info.Name; subNameLabel.Text = ToolBox.LimitString(loadedSub.Info.Name, subNameLabel.Font, subNameLabel.Rect.Width); - + CreateDummyCharacter(); - + cam.Position = Submarine.MainSub.Position + Submarine.MainSub.HiddenSubPosition; loadFrame = null; @@ -2822,10 +2843,10 @@ namespace Barotrauma cam.Position = Submarine.MainSub.Position + Submarine.MainSub.HiddenSubPosition; loadFrame = null; - + if (selectedSub.Info.GameVersion < new Version("0.8.9.0")) { - var adjustLightsPrompt = new GUIMessageBox(TextManager.Get("Warning"), TextManager.Get("AdjustLightsPrompt"), + var adjustLightsPrompt = new GUIMessageBox(TextManager.Get("Warning"), TextManager.Get("AdjustLightsPrompt"), new[] { TextManager.Get("Yes"), TextManager.Get("No") }); adjustLightsPrompt.Buttons[0].OnClicked += adjustLightsPrompt.Close; adjustLightsPrompt.Buttons[0].OnClicked += (btn, userdata) => @@ -2862,9 +2883,9 @@ namespace Barotrauma var msgBox = new GUIMessageBox( TextManager.Get("DeleteDialogLabel"), - TextManager.GetWithVariable("DeleteDialogQuestion", "[file]", sub.Name), + TextManager.GetWithVariable("DeleteDialogQuestion", "[file]", sub.Name), new string[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }); - msgBox.Buttons[0].OnClicked += (btn, userData) => + msgBox.Buttons[0].OnClicked += (btn, userData) => { try { @@ -2880,7 +2901,7 @@ namespace Barotrauma return true; }; msgBox.Buttons[0].OnClicked += msgBox.Close; - msgBox.Buttons[1].OnClicked += msgBox.Close; + msgBox.Buttons[1].OnClicked += msgBox.Close; } private void OpenEntityMenu(MapEntityCategory? entityCategory) @@ -2896,12 +2917,12 @@ namespace Barotrauma } selectedCategory = entityCategory; - + SetMode(Mode.Default); saveFrame = null; loadFrame = null; - + foreach (GUIComponent child in toggleEntityMenuButton.Children) { child.SpriteEffects = entityMenuOpen ? SpriteEffects.None : SpriteEffects.FlipVertically; @@ -2913,15 +2934,15 @@ namespace Barotrauma var innerList = child.GetChild(); foreach (GUIComponent grandChild in innerList.Content.Children) { - grandChild.Visible = true; + grandChild.Visible = true; } } - - if (!string.IsNullOrEmpty(entityFilterBox.Text)) - { - FilterEntities(entityFilterBox.Text); + + if (!string.IsNullOrEmpty(entityFilterBox.Text)) + { + FilterEntities(entityFilterBox.Text); } - + categorizedEntityList.UpdateScrollBarSize(); categorizedEntityList.BarScroll = 0.0f; // categorizedEntityList.Visible = true; @@ -2946,7 +2967,7 @@ namespace Barotrauma } }; categorizedEntityList.UpdateScrollBarSize(); - categorizedEntityList.BarScroll = 0.0f; + categorizedEntityList.BarScroll = 0.0f; return; } @@ -2955,7 +2976,7 @@ namespace Barotrauma filter = filter.ToLower(); foreach (GUIComponent child in allEntityList.Content.Children) { - child.Visible = + child.Visible = (!selectedCategory.HasValue || ((MapEntityPrefab)child.UserData).Category.HasFlag(selectedCategory)) && ((MapEntityPrefab)child.UserData).Name.ToLower().Contains(filter); } @@ -2989,7 +3010,7 @@ namespace Barotrauma MapEntity.DeselectAll(); MapEntity.FilteredSelectedList.Clear(); ClearUndoBuffer(); - + CreateDummyCharacter(); if (newMode == Mode.Wiring) { @@ -3005,14 +3026,14 @@ namespace Barotrauma dummyCharacter.Inventory.AllItems.ForEachMod(it => it.Remove()); dummyCharacter.Remove(); - dummyCharacter = null; + dummyCharacter = null; } private void CreateContextMenu() { if (GUIContextMenu.CurrentContextMenu != null) { return; } - List targets = MapEntity.mapEntityList.Any(me => me.IsHighlighted && !MapEntity.SelectedList.Contains(me)) ? + List targets = MapEntity.mapEntityList.Any(me => me.IsHighlighted && !MapEntity.SelectedList.Contains(me)) ? MapEntity.mapEntityList.Where(me => me.IsHighlighted).ToList() : new List(MapEntity.SelectedList); @@ -3139,7 +3160,7 @@ namespace Barotrauma break; } } - + bool setValues = true; object sliderMutex = new object(), sliderTextMutex = new object(), @@ -3208,7 +3229,7 @@ namespace Barotrauma Point areaSize = new Point(rect.Width, rect.Height / 2); Rectangle newColorRect = new Rectangle(rect.Location, areaSize); Rectangle oldColorRect = new Rectangle(new Point(newColorRect.Left, newColorRect.Bottom), areaSize); - + GUI.DrawRectangle(batch, newColorRect, ToolBox.HSVToRGB(colorPicker.SelectedHue, colorPicker.SelectedSaturation, colorPicker.SelectedValue), isFilled: true); GUI.DrawRectangle(batch, oldColorRect, originalColor, isFilled: true); GUI.DrawRectangle(batch, rect, Color.Black, isFilled: false); @@ -3218,10 +3239,10 @@ namespace Barotrauma hueScrollBar.OnMoved = (bar, scroll) => { SetColor(sliderMutex); return true; }; hueTextBox.OnValueChanged = input => { SetColor(sliderTextMutex); }; - + satScrollBar.OnMoved = (bar, scroll) => { SetColor(sliderMutex); return true; }; satTextBox.OnValueChanged = input => { SetColor(sliderTextMutex); }; - + valueScrollBar.OnMoved = (bar, scroll) => { SetColor(sliderMutex); return true; }; valueTextBox.OnValueChanged = input => { SetColor(sliderTextMutex); }; @@ -3273,7 +3294,7 @@ namespace Barotrauma } return true; }; - + cancelButton.OnClicked = (button, o) => { colorPicker.DisposeTextures(); @@ -3301,7 +3322,7 @@ namespace Barotrauma SetSliderTexts(hsv); SetColorPicker(hsv); SetHex(hsv); - } + } else if (source == sliderTextMutex) { Vector3 hsv = new Vector3(hueTextBox.FloatValue * 360f, satTextBox.FloatValue, valueTextBox.FloatValue); @@ -3315,7 +3336,7 @@ namespace Barotrauma SetSliders(hsv); SetSliderTexts(hsv); SetHex(hsv); - } + } else if (source == hexMutex) { Vector3 hsv = ToolBox.RGBToHSV(XMLExtensions.ParseColor(hexValueBox.Text, errorMessages: false)); @@ -3370,7 +3391,7 @@ namespace Barotrauma static string ColorToHex(Color color) => $"#{(color.R << 16 | color.G << 8 | color.B):X6}"; } - + private GUIFrame CreateWiringPanel() { GUIFrame frame = new GUIFrame(new RectTransform(new Vector2(0.03f, 0.35f), GUI.Canvas) @@ -3437,7 +3458,7 @@ namespace Barotrauma dummyCharacter.Inventory.TryPutItem(wire, slotIndex, false, false, dummyCharacter); return true; - + } /// @@ -3453,18 +3474,18 @@ namespace Barotrauma // We teleport our dummy character to the item so it appears as the entity stays still when in reality the dummy is holding it oldItemPosition = itemContainer.SimPosition; TeleportDummyCharacter(oldItemPosition); - + // Override this so we can be sure the container opens var container = itemContainer.GetComponent(); if (container != null) { container.KeepOpenWhenEquipped = true; } - + // We accept any slots except "Any" since that would take priority List allowedSlots = new List(); itemContainer.AllowedSlots.ForEach(type => { if (type != InvSlotType.Any) { allowedSlots.Add(type); } }); - + // Try to place the item in the dummy character's inventory bool success = dummyCharacter.Inventory.TryPutItem(itemContainer, dummyCharacter, allowedSlots); if (success) { OpenedItem = itemContainer; } @@ -3540,7 +3561,7 @@ namespace Barotrauma submarineDescriptionCharacterCount.Text = text.Length + " / " + submarineDescriptionLimit; } - + private bool SelectPrefab(GUIComponent component, object obj) { allEntityList.Deselect(); @@ -3548,7 +3569,7 @@ namespace Barotrauma if (GUI.MouseOn is GUIButton || GUI.MouseOn?.Parent is GUIButton) { return false; } AddPreviouslyUsed(obj as MapEntityPrefab); - + //if selecting a gap/hull/waypoint/spawnpoint, make sure the visibility is toggled on if (obj is CoreEntityPrefab prefab) { @@ -3574,14 +3595,14 @@ namespace Barotrauma { var itemInstance = LoadItemAssemblyInventorySafe(assemblyPrefab); var spawnedItem = false; - + itemInstance.ForEach(newItem => { if (newItem != null) { var placedItem = inv.TryPutItem(newItem, dummyCharacter); spawnedItem |= placedItem; - + if (!placedItem) { // Remove everything inside of the item so we don't get the popup asking if we want to keep the contained items @@ -3779,7 +3800,7 @@ namespace Barotrauma i--; } } - + foreach (MapEntity e in mapEntityList) { Rectangle entRect = e.WorldRect; @@ -3822,7 +3843,7 @@ namespace Barotrauma } } } - + for (int i = 0; i < hullRects.Count;) { Rectangle hullRect = hullRects[i]; @@ -3850,7 +3871,7 @@ namespace Barotrauma if (i >= hullRects.Count) break; } } - + for (int i = hullRects.Count-1; i >= 0;) { Rectangle hullRect = hullRects[i]; @@ -3878,7 +3899,7 @@ namespace Barotrauma if (i < 0) break; } } - + hullRects.Sort((a, b) => { if (a.X < b.X) return -1; @@ -3887,7 +3908,7 @@ namespace Barotrauma if (a.Y > b.Y) return 1; return 0; }); - + for (int i = 0; i < hullRects.Count - 1; i++) { Rectangle rect = hullRects[i]; @@ -3916,7 +3937,7 @@ namespace Barotrauma i--; } } - + for (int i = 0; i < hullRects.Count;i++) { Rectangle rect = hullRects[i]; @@ -3924,7 +3945,7 @@ namespace Barotrauma rect.Height += 32; hullRects[i] = rect; } - + hullRects.Sort((a, b) => { if (a.Y < b.Y) return -1; @@ -3933,7 +3954,7 @@ namespace Barotrauma if (a.X > b.X) return 1; return 0; }); - + for (int i = 0; i < hullRects.Count; i++) { for (int j = i+1; j < hullRects.Count; j++) @@ -3969,7 +3990,7 @@ namespace Barotrauma Gap newGap = new Gap(MapEntityPrefab.Find(null, "gap"), gapRect); } } - + public override void AddToGUIUpdateList() { if (GUI.DisableHUD) { return; } @@ -4013,7 +4034,7 @@ namespace Barotrauma saveFrame?.AddToGUIUpdateList(); } } - + /// /// GUI.MouseOn doesn't get updated while holding primary mouse and we need it to /// @@ -4023,7 +4044,7 @@ namespace Barotrauma return (EntityMenu?.MouseRect.Contains(PlayerInput.MousePosition) ?? false) || (entityCountPanel?.MouseRect.Contains(PlayerInput.MousePosition) ?? false) - || (MapEntity.EditingHUD?.MouseRect.Contains(PlayerInput.MousePosition) ?? false) + || (MapEntity.EditingHUD?.MouseRect.Contains(PlayerInput.MousePosition) ?? false) || (TopPanel?.MouseRect.Contains(PlayerInput.MousePosition) ?? false); } @@ -4087,6 +4108,8 @@ namespace Barotrauma { if (undoBufferPanel == null) { return; } + undoBufferDisclaimer.Visible = mode == Mode.Wiring; + undoBufferList.Content.Children.ForEachMod(component => { undoBufferList.Content.RemoveChild(component); @@ -4150,7 +4173,7 @@ namespace Barotrauma if (WiringMode && dummyCharacter != null) { - Wire equippedWire = + Wire equippedWire = Character.Controlled?.HeldItems.FirstOrDefault(it => it.GetComponent() != null)?.GetComponent() ?? Wire.DraggingWire; @@ -4168,9 +4191,9 @@ namespace Barotrauma } } } - + var highlightedEntities = new List(); - + // ReSharper disable once LoopCanBeConvertedToQuery foreach (Item item in MapEntity.mapEntityList.Where(entity => entity is Item).Cast()) { @@ -4178,11 +4201,11 @@ namespace Barotrauma if (wire == null || !wire.IsMouseOn()) { continue; } highlightedEntities.Add(item); } - + MapEntity.UpdateHighlighting(highlightedEntities, true); } } - + hullVolumeFrame.Visible = MapEntity.SelectedList.Any(s => s is Hull); hullVolumeFrame.RectTransform.AbsoluteOffset = new Point(Math.Max(showEntitiesPanel.Rect.Right, previouslyUsedPanel.Rect.Right), 0); saveAssemblyFrame.Visible = MapEntity.SelectedList.Count > 0; @@ -4200,11 +4223,11 @@ namespace Barotrauma else { var targetWithOffset = new Vector2(camTargetFocus.X, camTargetFocus.Y - offset / 2); - if (Math.Abs(cam.Position.X - targetWithOffset.X) < 1.0f && + if (Math.Abs(cam.Position.X - targetWithOffset.X) < 1.0f && Math.Abs(cam.Position.Y - targetWithOffset.Y) < 1.0f) { camTargetFocus = Vector2.Zero; - } + } else { cam.Position += (targetWithOffset - cam.Position) / cam.MoveSmoothness; @@ -4217,9 +4240,9 @@ namespace Barotrauma undoBufferList.Deselect(); } - if (GUI.KeyboardDispatcher.Subscriber == null - || MapEntity.EditingHUD != null - && GUI.KeyboardDispatcher.Subscriber is GUIComponent sub + if (GUI.KeyboardDispatcher.Subscriber == null + || MapEntity.EditingHUD != null + && GUI.KeyboardDispatcher.Subscriber is GUIComponent sub && MapEntity.EditingHUD.Children.Contains(sub)) { if (PlayerInput.IsCtrlDown() && !WiringMode) @@ -4229,7 +4252,7 @@ namespace Barotrauma // Ctrl+Shift+Z redos while Ctrl+Z undos if (PlayerInput.IsShiftDown()) { Redo(1); } else { Undo(1); } } - + // ctrl+Y redo if (PlayerInput.KeyHit(Keys.Y)) { @@ -4288,7 +4311,7 @@ namespace Barotrauma } } } - + // Focus to selection if (PlayerInput.KeyHit(Keys.F) && mode == Mode.Default) { @@ -4310,7 +4333,7 @@ namespace Barotrauma camTargetFocus = rect.Center.ToVector2(); } } - + if (GameMain.Config.KeyBind(InputType.ToggleInventory).IsHit() && mode == Mode.Default) { toggleEntityMenuButton.OnClicked?.Invoke(toggleEntityMenuButton, toggleEntityMenuButton.UserData); @@ -4396,7 +4419,7 @@ namespace Barotrauma lightComponent.LightColor; lightComponent.Light.LightSpriteEffect = lightComponent.Item.SpriteEffects; } - } + } GameMain.LightManager?.Update((float)deltaTime); } @@ -4432,7 +4455,7 @@ namespace Barotrauma }); } - if (dummyCharacter.SelectedConstruction == null || + if (dummyCharacter.SelectedConstruction == null || dummyCharacter.SelectedConstruction.GetComponent() != null) { if (WiringMode && PlayerInput.IsShiftDown()) @@ -4449,12 +4472,12 @@ namespace Barotrauma var (cursorX, cursorY) = dummyCharacter.CursorPosition; bool isHorizontal = Math.Abs(cursorX - lastNode.X) < Math.Abs(cursorY - lastNode.Y); - + float roundedY = MathUtils.Round(cursorY, Submarine.GridSize.Y / 2.0f); float roundedX = MathUtils.Round(cursorX, Submarine.GridSize.X / 2.0f); - dummyCharacter.CursorPosition = isHorizontal - ? new Vector2(lastNode.X, roundedY) + dummyCharacter.CursorPosition = isHorizontal + ? new Vector2(lastNode.X, roundedY) : new Vector2(roundedX, lastNode.Y); } } @@ -4464,7 +4487,7 @@ namespace Barotrauma { TeleportDummyCharacter(oldItemPosition); } - + if (WiringMode && dummyCharacter?.SelectedConstruction == null) { TeleportDummyCharacter(FarseerPhysics.ConvertUnits.ToSimUnits(dummyCharacter.CursorPosition)); @@ -4486,31 +4509,31 @@ namespace Barotrauma if (inv?.visualSlots != null && !PlayerInput.IsCtrlDown()) { var dragginMouse = MouseDragStart != Vector2.Zero && Vector2.Distance(PlayerInput.MousePosition, MouseDragStart) >= GUI.Scale * 20; - + // So we don't accidentally drag inventory items while doing this if (DraggedItemPrefab != null) { Inventory.DraggingItems.Clear(); } - - switch (DraggedItemPrefab) + + switch (DraggedItemPrefab) { // regular item prefabs - case ItemPrefab itemPrefab when PlayerInput.PrimaryMouseButtonClicked() || dragginMouse: + case ItemPrefab itemPrefab when PlayerInput.PrimaryMouseButtonClicked() || dragginMouse: { bool spawnedItem = false; for (var i = 0; i < inv.Capacity; i++) { var slot = inv.visualSlots[i]; var itemContainer = inv.GetItemAt(i)?.GetComponent(); - + // check if the slot is empty or if we can place the item into a container, for example an oxygen tank into a diving suit if (Inventory.IsMouseOnSlot(slot)) { var newItem = new Item(itemPrefab, Vector2.Zero, Submarine.MainSub); - + if (inv.CanBePutInSlot(itemPrefab, i, condition: null)) { bool placedItem = inv.TryPutItem(newItem, i, false, true, dummyCharacter); spawnedItem |= placedItem; - + if (!placedItem) { newItem.Remove(); @@ -4520,7 +4543,7 @@ namespace Barotrauma { bool placedItem = itemContainer.Inventory.TryPutItem(newItem, dummyCharacter); spawnedItem |= placedItem; - + // try to place the item into the inventory of the item we are hovering over if (!placedItem) { @@ -4564,28 +4587,28 @@ namespace Barotrauma { // load the items var itemInstance = LoadItemAssemblyInventorySafe(assemblyPrefab); - + // counter for items that failed so we so we known that slot remained empty var failedCount = 0; - + for (var j = 0; j < itemInstance.Count(); j++) { var newItem = itemInstance[j]; var newSpot = i + j - failedCount; - + // try to find a valid slot to put the items - while (inv.visualSlots.Length > newSpot) + while (inv.visualSlots.Length > newSpot) { if (inv.GetItemAt(newSpot) == null) { break; } newSpot++; } - + // valid slot found if (inv.visualSlots.Length > newSpot) { var placedItem = inv.TryPutItem(newItem, newSpot, false, true, dummyCharacter); spawnedItems |= placedItem; - + if (!placedItem) { failedCount++; @@ -4598,7 +4621,7 @@ namespace Barotrauma { var placedItem = inv.TryPutItem(newItem, dummyCharacter); spawnedItems |= placedItem; - + // if our while loop didn't find a valid slot then let the inventory decide where to put it as a last resort if (!placedItem) { @@ -4664,11 +4687,11 @@ namespace Barotrauma MeasurePositionStart = cam.ScreenToWorld(PlayerInput.MousePosition); } } - + if (!WiringMode) { bool shouldCloseHud = dummyCharacter?.SelectedConstruction != null && HUD.CloseHUD(dummyCharacter.SelectedConstruction.Rect) && DraggedItemPrefab == null; - + if (MapEntityPrefab.Selected != null && GUI.MouseOn == null) { MapEntityPrefab.Selected.UpdatePlacing(cam); @@ -4685,7 +4708,7 @@ namespace Barotrauma { if (dummyCharacter?.SelectedConstruction == null) { - CreateContextMenu(); + CreateContextMenu(); } DraggedItemPrefab = null; } @@ -4695,11 +4718,11 @@ namespace Barotrauma { CloseItem(); } - } + } MapEntity.UpdateEditor(cam, (float)deltaTime); } - entityMenuOpenState = entityMenuOpen && !WiringMode ? + entityMenuOpenState = entityMenuOpen && !WiringMode ? (float)Math.Min(entityMenuOpenState + deltaTime * 5.0f, 1.0f) : (float)Math.Max(entityMenuOpenState - deltaTime * 5.0f, 0.0f); @@ -4723,7 +4746,7 @@ namespace Barotrauma { saveFrame = null; } - } + } if (dummyCharacter != null) { @@ -4789,28 +4812,28 @@ namespace Barotrauma GUI.DrawLine(spriteBatch, new Vector2(Submarine.MainSub.HiddenSubPosition.X, -cam.WorldView.Y), new Vector2(Submarine.MainSub.HiddenSubPosition.X, -(cam.WorldView.Y - cam.WorldView.Height)), Color.White * 0.5f, 1.0f, (int)(2.0f / cam.Zoom)); GUI.DrawLine(spriteBatch, new Vector2(cam.WorldView.X, -Submarine.MainSub.HiddenSubPosition.Y), new Vector2(cam.WorldView.Right, -Submarine.MainSub.HiddenSubPosition.Y), Color.White * 0.5f, 1.0f, (int)(2.0f / cam.Zoom)); } - Submarine.DrawBack(spriteBatch, true, e => - e is Structure s && - !IsSubcategoryHidden(e.prefab?.Subcategory) && + Submarine.DrawBack(spriteBatch, true, e => + e is Structure s && + !IsSubcategoryHidden(e.prefab?.Subcategory) && (e.SpriteDepth >= 0.9f || s.Prefab.BackgroundSprite != null)); Submarine.DrawPaintedColors(spriteBatch, true); spriteBatch.End(); spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, transformMatrix: cam.Transform); - + // When we "open" a wearable item with inventory it won't get rendered because the dummy character is invisible // So we are drawing a clone of it on the same position if (OpenedItem?.GetComponent() != null) { - OpenedItem.Sprite.Draw(spriteBatch, new Vector2(OpenedItem.DrawPosition.X, -(OpenedItem.DrawPosition.Y)), + OpenedItem.Sprite.Draw(spriteBatch, new Vector2(OpenedItem.DrawPosition.X, -(OpenedItem.DrawPosition.Y)), scale: OpenedItem.Scale, color: OpenedItem.SpriteColor, depth: OpenedItem.SpriteDepth); GUI.DrawRectangle(spriteBatch, new Vector2(OpenedItem.WorldRect.X, -OpenedItem.WorldRect.Y), new Vector2(OpenedItem.Rect.Width, OpenedItem.Rect.Height), Color.White, false, 0, (int)Math.Max(2.0f / cam.Zoom, 1.0f)); } - - Submarine.DrawBack(spriteBatch, true, e => + + Submarine.DrawBack(spriteBatch, true, e => (!(e is Structure) || e.SpriteDepth < 0.9f) && !IsSubcategoryHidden(e.prefab?.Subcategory)); spriteBatch.End(); @@ -4876,7 +4899,7 @@ namespace Barotrauma } } } - + if (dummyCharacter != null) { if (WiringMode) @@ -4913,7 +4936,7 @@ namespace Barotrauma Vector2 offset = new Vector2(GUI.IntScale(24)); GUI.DrawString(spriteBatch, PlayerInput.MousePosition + offset, $"{realWorldDistance}m", GUI.Style.TextColor, font: GUI.SubHeadingFont, backgroundColor: Color.Black, backgroundPadding: 4); } - + spriteBatch.End(); } @@ -4950,13 +4973,13 @@ namespace Barotrauma Submarine.DrawFront(spriteBatch); Submarine.DrawDamageable(spriteBatch, null); spriteBatch.End(); - + GameMain.Instance.GraphicsDevice.SetRenderTarget(null); rt.SaveAsPng(stream, width, height); } - //for some reason setting the rendertarget changes the size of the viewport + //for some reason setting the rendertarget changes the size of the viewport //but it doesn't change back to default when setting it back to null GameMain.Instance.ResetViewPort(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index 986afef6d..1fc36bc42 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -271,6 +271,21 @@ namespace Barotrauma } } } + else if (newValue is string[] a) + { + for (int i = 0; i < fields.Length; i++) + { + if (i >= a.Length) { break; } + if (fields[i] is GUITextBox textBox) + { + textBox.Text = a[i]; + if (flash) + { + textBox.Flash(GUI.Style.Green); + } + } + } + } } public SerializableEntityEditor(RectTransform parent, ISerializableEntity entity, bool inGame, bool showName, string style = "", int elementHeight = 24, ScalableFont titleFont = null) @@ -423,6 +438,10 @@ namespace Barotrauma { propertyField = CreateRectangleField(entity, property, r, displayName, toolTip); } + else if(value is string[] a) + { + propertyField = CreateStringArrayField(entity, property, a, displayName, toolTip); + } return propertyField; } @@ -1164,6 +1183,75 @@ namespace Barotrauma return frame; } + public GUIComponent CreateStringArrayField(ISerializableEntity entity, SerializableProperty property, string[] value, string displayName, string toolTip) + { + int elementCount = (value.Length + 1); + var frame = new GUIFrame(new RectTransform(new Point(Rect.Width, elementCount * elementHeight), layoutGroup.RectTransform, isFixedSize: true), color: Color.Transparent); + var label = new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f / elementCount), frame.RectTransform), displayName, font: GUI.SmallFont) + { + ToolTip = toolTip + }; + var editableAttribute = property.GetAttribute(); + var fields = new GUIComponent[value.Length]; + var inputArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, (float)(elementCount - 1) / elementCount), frame.RectTransform, anchor: Anchor.BottomLeft)) + { + RelativeSpacing = 0.01f + }; + elementCount -= 1; + + for (int i = 0; i < value.Length; i++) + { + var element = new GUIFrame(new RectTransform(new Vector2(1.0f, 1.0f / elementCount), inputArea.RectTransform) { MinSize = new Point(50, 0), MaxSize = new Point((int)(0.9f * inputArea.Rect.Width), 50) }, style: null); + var elementLayoutGroup = new GUILayoutGroup(new RectTransform(Vector2.One, element.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + // Set the label to be (i + 1) so it's easier to understand for non-programmers + string componentLabel = (i + 1).ToString(); + new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), elementLayoutGroup.RectTransform) { MaxSize = new Point(25, elementLayoutGroup.Rect.Height) }, componentLabel, font: GUI.SmallFont, textAlignment: Alignment.Center); + GUITextBox textBox = new GUITextBox(new RectTransform(new Vector2(0.7f, 1), elementLayoutGroup.RectTransform), text: value[i]) { Font = GUI.SmallFont }; + int comp = i; + textBox.OnEnterPressed += (textBox, text) => OnApply(textBox); + textBox.OnDeselected += (textBox, keys) => OnApply(textBox); + fields[i] = textBox; + + bool OnApply(GUITextBox textBox) + { + // Reserve the semicolon for serializing the value + bool containsForbiddenCharacters = textBox.Text.Contains(';'); + string[] newValue = (string[])property.GetValue(entity); + if (!containsForbiddenCharacters) + { + newValue[comp] = textBox.Text; + if (SetPropertyValue(property, entity, newValue)) + { + TrySendNetworkUpdate(entity, property); + textBox.Flash(color: GUI.Style.Green, flashDuration: 1f); + } + } + else + { + textBox.Text = newValue[comp]; + textBox.Flash(color: GUI.Style.Red, flashDuration: 1f); + } + return true; + } + } + + refresh += () => + { + if (fields.None(f => ((GUITextBox)f).Selected)) + { + string[] value = (string[])property.GetValue(entity); + for (int i = 0; i < fields.Length; i++) + { + ((GUITextBox)fields[i]).Text = value[i]; + } + } + }; + + frame.RectTransform.MinSize = new Point(0, frame.RectTransform.Children.Sum(c => c.MinSize.Y)); + if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name, fields); } + return frame; + } + public void CreateTextPicker(string textTag, ISerializableEntity entity, SerializableProperty property, GUITextBox textBox) { var msgBox = new GUIMessageBox("", "", new string[] { TextManager.Get("Cancel") }, new Vector2(0.2f, 0.5f), new Point(300, 400)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs index fbd068958..818c5040e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs @@ -829,7 +829,6 @@ namespace Barotrauma GameMain.GameSession.EventManager.CurrentIntensity * 100.0f : 0.0f; IEnumerable suitableMusic = GetSuitableMusicClips(currentMusicType, currentIntensity); - int mainTrackIndex = 0; if (suitableMusic.Count() == 0) { @@ -861,7 +860,6 @@ namespace Barotrauma IEnumerable suitableNoiseLoops = Screen.Selected == GameMain.GameScreen ? GetSuitableMusicClips(Level.Loaded.LevelData?.Biome?.Identifier, currentIntensity) : Enumerable.Empty(); - if (suitableNoiseLoops.Count() == 0) { targetMusic[noiseLoopIndex] = null; @@ -877,12 +875,23 @@ namespace Barotrauma targetMusic[noiseLoopIndex] = null; } + IEnumerable suitableTypeAmbiences = GetSuitableMusicClips($"{currentMusicType}ambience", currentIntensity); + int typeAmbienceTrackIndex = 2; + if (suitableTypeAmbiences.None()) + { + targetMusic[typeAmbienceTrackIndex] = null; + } + // Switch the type ambience if nothing playing atm or the currently playing clip is not suitable anymore + else if (targetMusic[typeAmbienceTrackIndex] == null || currentMusic[typeAmbienceTrackIndex] == null || !currentMusic[typeAmbienceTrackIndex].IsPlaying() || suitableTypeAmbiences.None(m => m.File == currentMusic[typeAmbienceTrackIndex].Filename)) + { + targetMusic[mainTrackIndex] = suitableMusic.GetRandom(); + } + //get the appropriate intensity layers for current situation IEnumerable suitableIntensityMusic = Screen.Selected == GameMain.GameScreen ? GetSuitableMusicClips("intensity", currentIntensity) : Enumerable.Empty(); - - int intensityTrackStartIndex = 2; + int intensityTrackStartIndex = 3; for (int i = intensityTrackStartIndex; i < MaxMusicChannels; i++) { //disable targetmusics that aren't suitable anymore @@ -891,7 +900,6 @@ namespace Barotrauma targetMusic[i] = null; } } - foreach (BackgroundMusic intensityMusic in suitableIntensityMusic) { //already playing, do nothing diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index cf1f1eb9d..d37b222fb 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.1500.4.0 + 0.1500.5.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index af5c2accc..068f93785 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.1500.4.0 + 0.1500.5.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 70656497c..bc48977e1 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.1500.4.0 + 0.1500.5.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index d0a633e79..dd27b108a 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.1500.4.0 + 0.1500.5.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 19f20cd27..6dcdf7a5f 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.1500.4.0 + 0.1500.5.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs index 3729108f9..04fa5c705 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs @@ -12,6 +12,7 @@ namespace Barotrauma partial void OnSkillChanged(string skillIdentifier, float prevLevel, float newLevel, Vector2 textPopupPos) { + if (Character == null || Character.Removed) { return; } if (!prevSentSkill.ContainsKey(skillIdentifier)) { prevSentSkill[skillIdentifier] = prevLevel; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/AlienRuinMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/AlienRuinMission.cs new file mode 100644 index 000000000..4723cb072 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/AlienRuinMission.cs @@ -0,0 +1,21 @@ +using Barotrauma.Networking; + +namespace Barotrauma +{ + partial class AlienRuinMission : Mission + { + public override void ServerWriteInitial(IWriteMessage msg, Client c) + { + msg.Write((ushort)existingTargets.Count); + foreach (var t in existingTargets) + { + msg.Write(t != null ? t.ID : Entity.NullEntityID); + } + msg.Write((ushort)spawnedTargets.Count); + foreach (var t in spawnedTargets) + { + t.WriteSpawnData(msg, t.ID, false); + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/Mission.cs index dbbc96902..4faee3e40 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/Mission.cs @@ -17,5 +17,10 @@ namespace Barotrauma } public abstract void ServerWriteInitial(IWriteMessage msg, Client c); + + public virtual void ServerWrite(IWriteMessage msg) + { + msg.Write((ushort)State); + } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/ScanMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/ScanMission.cs new file mode 100644 index 000000000..3d3e4f171 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/ScanMission.cs @@ -0,0 +1,36 @@ +using Barotrauma.Networking; + +namespace Barotrauma +{ + partial class ScanMission : Mission + { + public override void ServerWriteInitial(IWriteMessage msg, Client c) + { + msg.Write((ushort)startingItems.Count); + foreach (var item in startingItems) + { + item.WriteSpawnData(msg, + item.ID, + parentInventoryIDs.ContainsKey(item) ? parentInventoryIDs[item] : Entity.NullEntityID, + parentItemContainerIndices.ContainsKey(item) ? parentItemContainerIndices[item] : (byte)0); + } + ServerWriteScanTargetStatus(msg); + } + + public override void ServerWrite(IWriteMessage msg) + { + base.ServerWrite(msg); + ServerWriteScanTargetStatus(msg); + } + + private void ServerWriteScanTargetStatus(IWriteMessage msg) + { + msg.Write((byte)scanTargets.Count); + foreach (var kvp in scanTargets) + { + msg.Write(kvp.Key != null ? kvp.Key.ID : Entity.NullEntityID); + msg.Write(kvp.Value); + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Scanner.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Scanner.cs new file mode 100644 index 000000000..337b721b0 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Scanner.cs @@ -0,0 +1,14 @@ +using Barotrauma.Networking; + +namespace Barotrauma.Items.Components +{ + partial class Scanner : ItemComponent, IServerSerializable + { + private float LastSentScanTimer { get; set; } + + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + { + msg.Write(scanTimer); + } + } +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ButtonTerminal.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ButtonTerminal.cs new file mode 100644 index 000000000..056410165 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ButtonTerminal.cs @@ -0,0 +1,21 @@ +using Barotrauma.Networking; + +namespace Barotrauma.Items.Components +{ + partial class ButtonTerminal : ItemComponent, IClientSerializable, IServerSerializable + { + public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) + { + int signalIndex = msg.ReadRangedInteger(0, Signals.Length - 1); + if (!item.CanClientAccess(c)) { return; } + if (!SendSignal(signalIndex)) { return; } + GameServer.Log($"{GameServer.CharacterLogName(c.Character)} sent a signal \"{Signals[signalIndex]}\" from {item.Name}", ServerLog.MessageType.ItemInteraction); + item.CreateServerEvent(this, new object[] { signalIndex }); + } + + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + { + Write(msg, extraData); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index cedb86f86..ad6a7c6e9 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -3791,7 +3791,7 @@ namespace Barotrauma.Networking return preferredClient; } - public void UpdateMissionState(Mission mission, int state) + public void UpdateMissionState(Mission mission) { foreach (var client in connectedClients) { @@ -3799,7 +3799,7 @@ namespace Barotrauma.Networking msg.Write((byte)ServerPacketHeader.MISSION); int missionIndex = GameMain.GameSession.GetMissionIndex(mission); msg.Write((byte)(missionIndex == -1 ? 255: missionIndex)); - msg.Write((ushort)state); + mission?.ServerWrite(msg); serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index 5430e285c..8fbaabece 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -8,6 +8,11 @@ namespace Barotrauma.Networking { partial class RespawnManager : Entity, IServerSerializable { + /// + /// How much skills drop towards the job's default skill levels when respawning midround in the campaign + /// + const float SkillReductionOnCampaignMidroundRespawn = 0.5f; + private DateTime despawnTime; private float shuttleEmptyTimer; @@ -361,9 +366,21 @@ namespace Barotrauma.Networking if (!bot && campaign != null) { var matchingData = campaign?.GetClientCharacterData(clients[i]); - if (matchingData != null && !matchingData.HasSpawned) + if (matchingData != null) { - forceSpawnInMainSub = true; + if (!matchingData.HasSpawned) + { + forceSpawnInMainSub = true; + } + else + { + foreach (Skill skill in characterInfos[i].Job.Skills) + { + var skillPrefab = characterInfos[i].Job.Prefab.Skills.Find(s => skill.Prefab == s); + if (skillPrefab == null) { continue; } + skill.Level = MathHelper.Lerp(skill.Level, skillPrefab.LevelRange.X, SkillReductionOnCampaignMidroundRespawn); + } + } } } diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index ab6d66a0c..dcd61be75 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.1500.4.0 + 0.1500.5.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml index 3a605f435..6e068a3f0 100644 --- a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml +++ b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml @@ -80,6 +80,7 @@ + @@ -87,6 +88,8 @@ + + @@ -118,7 +121,6 @@ - @@ -170,6 +172,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs index 3635b8e7e..daea35d55 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs @@ -104,6 +104,8 @@ namespace Barotrauma (!requireNonDirty || !pathSteering.IsPathDirty); protected readonly float colliderWidth; + protected readonly float minGapSize; + protected readonly float minHullSize; protected readonly float colliderLength; protected readonly float avoidLookAheadDistance; @@ -116,6 +118,8 @@ namespace Barotrauma colliderWidth = size.X; colliderLength = size.Y; avoidLookAheadDistance = Math.Max(Math.Max(colliderWidth, colliderLength) * 3, 1.5f); + minGapSize = ConvertUnits.ToDisplayUnits(Math.Min(colliderWidth, colliderLength)); + minHullSize = ConvertUnits.ToDisplayUnits(Math.Max(colliderLength, colliderWidth) * 1.1f); } public virtual void OnAttacked(Character attacker, AttackResult attackResult) { } @@ -390,8 +394,7 @@ namespace Barotrauma else { if (gap.Open < 1) { continue; } - bool canGetThrough = ConvertUnits.ToDisplayUnits(colliderWidth) < gap.Size; - if (!canGetThrough) { continue; } + if (gap.Size < minGapSize) { continue; } } if (gap.FlowTargetHull == Character.CurrentHull) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs index 7479ae3da..b105f540e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs @@ -92,7 +92,16 @@ namespace Barotrauma public string SonarLabel; public string SonarIconIdentifier; - public bool Enabled => SoundRange > 0 || SightRange > 0; + private bool inDetectable; + + /// + /// Should be reset to false each frame and kept indetectable by e.g. a status effect. + /// + public bool InDetectable + { + get => inDetectable || (SoundRange <= 0 && SightRange <= 0); + set => inDetectable = value; + } public float MinSoundRange, MinSightRange; public float MaxSoundRange = 100000, MaxSightRange = 100000; @@ -181,14 +190,15 @@ namespace Barotrauma public void Update(float deltaTime) { - if (Enabled && !Static && FadeOutTime > 0) + InDetectable = false; + if (!Static && FadeOutTime > 0) { // The aitarget goes silent/invisible if the components don't keep it active - if (!StaticSight) + if (!StaticSight && SightRange > 0) { DecreaseSightRange(deltaTime); } - if (!StaticSound) + if (!StaticSound && SoundRange > 0) { DecreaseSoundRange(deltaTime); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 241cd47a8..a59a2e529 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -11,7 +11,7 @@ using System.Linq; namespace Barotrauma { - public enum AIState { Idle, Attack, Escape, Eat, Flee, Avoid, Aggressive, PassiveAggressive, Protect, Observe, Freeze, Follow } + public enum AIState { Idle, Attack, Escape, Eat, Flee, Avoid, Aggressive, PassiveAggressive, Protect, Observe, Freeze, Follow, FleeTo, Patrol } public enum AttackPattern { Straight, Sweep, Circle } @@ -89,6 +89,10 @@ namespace Barotrauma { _previousAttackingLimb = _attackingLimb; } + if (_attackingLimb != null && value != _attackingLimb && _attackingLimb.attack.CoolDownTimer > 0) + { + SetAimTimer(); + } _attackingLimb = value; attackVector = null; Reverse = _attackingLimb != null && _attackingLimb.attack.Reverse; @@ -175,7 +179,7 @@ namespace Barotrauma get { //can't enter a submarine when attached to something - return Character.AnimController.CanEnterSubmarine && (LatchOntoAI == null || !LatchOntoAI.IsAttached); + return Character.AnimController.CanEnterSubmarine && (LatchOntoAI == null || !LatchOntoAI.IsAttachedToSub); } } @@ -186,7 +190,7 @@ namespace Barotrauma //can't flip when attached to something, when eating, or reversing or in a (relatively) small room return !Reverse && (State != AIState.Eat || Character.SelectedCharacter == null) && - (LatchOntoAI == null || !LatchOntoAI.IsAttached) && + (LatchOntoAI == null || !LatchOntoAI.IsAttachedToSub) && (Character.CurrentHull == null || !Character.AnimController.InWater || Math.Min(Character.CurrentHull.Size.X, Character.CurrentHull.Size.Y) > ConvertUnits.ToDisplayUnits(Math.Max(colliderLength, colliderWidth))); } } @@ -294,7 +298,7 @@ namespace Barotrauma ReevaluateAttacks(); outsideSteering = new SteeringManager(this); - insideSteering = new IndoorsSteeringManager(this, Character.IsHumanoid, canAttackDoors); + insideSteering = new IndoorsSteeringManager(this, Character.Params.AI.CanOpenDoors, canAttackDoors); steeringManager = outsideSteering; State = AIState.Idle; @@ -405,11 +409,20 @@ namespace Barotrauma private float movementMargin; + private void ReleaseDragTargets() + { + if (Character.Inventory != null) + { + Character.HeldItems.ForEach(i => i.GetComponent()?.GetRope()?.Snap()); + } + } + public override void Update(float deltaTime) { if (DisableEnemyAI) { return; } base.Update(deltaTime); UpdateTriggers(deltaTime); + Character.ClearInputs(); bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f && (-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X)); if (steeringManager == insideSteering) @@ -446,10 +459,9 @@ namespace Barotrauma Character.AnimController.TargetDir = Character.AnimController.movement.X > 0.0f ? Direction.Right : Direction.Left; } } - if (isStateChanged) { - if (State == AIState.Idle) + if (State == AIState.Idle || State == AIState.Patrol) { stateResetTimer -= deltaTime; if (stateResetTimer <= 0) @@ -509,7 +521,9 @@ namespace Barotrauma selectedTargetingParams = targetingParams; State = targetingParams.State; } - if (SelectedAiTarget?.Entity != null && !IsLatchedOnSub && State == AIState.Attack || State == AIState.Aggressive || State == AIState.PassiveAggressive) + if (SelectedAiTarget?.Entity != null && + (LatchOntoAI == null || !LatchOntoAI.IsAttached || wallTarget != null) && + (State == AIState.Attack || State == AIState.Aggressive || State == AIState.PassiveAggressive)) { UpdateWallTarget(requiredHoleCount); } @@ -570,6 +584,9 @@ namespace Barotrauma case AIState.Idle: UpdateIdle(deltaTime); break; + case AIState.Patrol: + UpdatePatrol(deltaTime); + break; case AIState.Attack: run = !IsCoolDownRunning || AttackingLimb != null && AttackingLimb.attack.FullSpeedAfterAttack; UpdateAttack(deltaTime); @@ -632,6 +649,7 @@ namespace Barotrauma break; case AIState.Protect: case AIState.Follow: + case AIState.FleeTo: if (SelectedAiTarget == null || SelectedAiTarget.Entity == null || SelectedAiTarget.Entity.Removed) { State = AIState.Idle; @@ -647,7 +665,7 @@ namespace Barotrauma if (c.IsDead || c.Removed) { return false; } if (!Character.IsFriendly(c)) { return true; } // Only apply the threshold to friendly characters - return a.Damage >= selectedTargetingParams.DamageThreshold; + return a.Damage >= selectedTargetingParams.Threshold; } Character attacker = targetCharacter.LastAttackers.LastOrDefault(IsValid)?.Character; if (attacker != null) @@ -663,14 +681,33 @@ namespace Barotrauma float reactDist = selectedTargetingParams != null && selectedTargetingParams.ReactDistance > 0 ? selectedTargetingParams.ReactDistance : GetPerceivingRange(SelectedAiTarget); if (sqrDist > Math.Pow(reactDist + movementMargin, 2)) { - movementMargin = reactDist; + movementMargin = State == AIState.FleeTo ? 0 : reactDist; run = true; UpdateFollow(deltaTime); } else { movementMargin = MathHelper.Clamp(movementMargin -= deltaTime, 0, reactDist); - UpdateIdle(deltaTime); + if (State == AIState.FleeTo) + { + SteeringManager.Reset(); + Character.AnimController.TargetMovement = Vector2.Zero; + float force = Character.AnimController.SwimSlowParams.SteerTorque; + Character.AnimController.Collider.MoveToPos(SelectedAiTarget.Entity.SimPosition, force); + if (SelectedAiTarget.Entity is Item item) + { + Character.AnimController.Collider.SmoothRotate(MathHelper.ToRadians(item.Rotation), force); + } + Character.AnimController.ApplyPose( + new Vector2(0, -1), + new Vector2(0, -1), + new Vector2(0, -1), + new Vector2(0, -1), footMoveForce: 1); + } + else + { + UpdateIdle(deltaTime); + } } break; case AIState.Observe: @@ -763,6 +800,10 @@ namespace Barotrauma private void UpdateIdle(float deltaTime, bool followLastTarget = true) { + if (AIParams.PatrolFlooded || AIParams.PatrolDry) + { + State = AIState.Patrol; + } var pathSteering = SteeringManager as IndoorsSteeringManager; if (pathSteering == null) { @@ -820,6 +861,138 @@ namespace Barotrauma } } + private readonly List targetHulls = new List(); + private readonly List hullWeights = new List(); + + private Hull patrolTarget; + private float newPatrolTargetTimer; + private float patrolTimerMargin; + private readonly float newPatrolTargetIntervalMin = 5; + private readonly float newPatrolTargetIntervalMax = 30; + private bool searchingNewHull; + + private void UpdatePatrol(float deltaTime, bool followLastTarget = true) + { + if (SteeringManager is IndoorsSteeringManager pathSteering) + { + if (patrolTarget == null || + pathSteering.CurrentPath == null || + !pathSteering.IsPathDirty && (pathSteering.CurrentPath.Finished || pathSteering.CurrentPath.Unreachable)) + { + newPatrolTargetTimer = Math.Min(newPatrolTargetTimer, newPatrolTargetIntervalMin); + } + if (newPatrolTargetTimer > 0) + { + newPatrolTargetTimer -= deltaTime; + } + else + { + if (!searchingNewHull) + { + searchingNewHull = true; + FindTargetHulls(); + } + else if (targetHulls.Any()) + { + patrolTarget = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced); + var path = PathSteering.PathFinder.FindPath(Character.SimPosition, patrolTarget.SimPosition, minGapSize: minGapSize * 1.5f, nodeFilter: n => NodeFilter(n) && PatrolNodeFilter(n)); + + if (path.Unreachable) + { + //can't go to this room, remove it from the list and try another room + int index = targetHulls.IndexOf(patrolTarget); + targetHulls.RemoveAt(index); + hullWeights.RemoveAt(index); + PathSteering.Reset(); + patrolTarget = null; + patrolTimerMargin += 0.5f; + patrolTimerMargin = Math.Min(patrolTimerMargin, newPatrolTargetIntervalMin); + newPatrolTargetTimer = Math.Min(newPatrolTargetIntervalMin, patrolTimerMargin); + } + else + { + PathSteering.SetPath(path); + patrolTimerMargin = 0; + newPatrolTargetTimer = newPatrolTargetIntervalMax * Rand.Range(0.5f, 1.5f); + searchingNewHull = false; + } + } + else + { + // Couldn't find a valid hull + newPatrolTargetTimer = newPatrolTargetIntervalMax; + searchingNewHull = false; + } + } + if (patrolTarget != null && pathSteering.CurrentPath != null && !pathSteering.CurrentPath.Finished && !pathSteering.CurrentPath.Unreachable) + { + PathSteering.SteeringSeek(Character.GetRelativeSimPosition(patrolTarget), weight: 1, minGapWidth: minGapSize * 1.5f, nodeFilter: n => NodeFilter(n) && PatrolNodeFilter(n)); + return; + } + } + + bool PatrolNodeFilter(PathNode n) => + AIParams.PatrolFlooded && (Character.CurrentHull == null || n.Waypoint.CurrentHull == null || n.Waypoint.CurrentHull.WaterPercentage >= 80) || + AIParams.PatrolDry && Character.CurrentHull != null && n.Waypoint.CurrentHull != null && n.Waypoint.CurrentHull.WaterPercentage <= 50; + + UpdateIdle(deltaTime, followLastTarget); + } + + private bool NodeFilter(PathNode n) => n.Waypoint.CurrentHull == null || n.Waypoint.CurrentHull.Rect.Width > minHullSize && n.Waypoint.CurrentHull.Rect.Height > minHullSize; + + private void FindTargetHulls() + { + if (Character.Submarine == null) { return; } + if (Character.CurrentHull == null) { return; } + targetHulls.Clear(); + hullWeights.Clear(); + float hullMinSize = ConvertUnits.ToDisplayUnits(Math.Max(colliderLength, colliderWidth) * 2); + bool checkWaterLevel = !AIParams.PatrolFlooded || !AIParams.PatrolDry; + foreach (var hull in Hull.hullList) + { + if (hull.Submarine == null) { continue; } + if (hull.Submarine.TeamID != Character.Submarine.TeamID) { continue; } + if (!Character.Submarine.IsConnectedTo(hull.Submarine)) { continue; } + if (hull.RectWidth < hullMinSize || hull.RectHeight < hullMinSize) { continue; } + if (checkWaterLevel) + { + if (AIParams.PatrolDry) + { + if (hull.WaterPercentage > 50) { continue; } + } + if (AIParams.PatrolFlooded) + { + if (hull.WaterPercentage < 80) { continue; } + } + } + if (AIParams.PatrolDry && hull.WaterPercentage < 80) + { + if (Math.Abs(Character.CurrentHull.WorldPosition.Y - hull.WorldPosition.Y) > Character.CurrentHull.CeilingHeight / 2) + { + // Ignore dry hulls that are on a different level + continue; + } + } + if (!targetHulls.Contains(hull)) + { + targetHulls.Add(hull); + float weight = hull.Size.Combine(); + float dist = Vector2.Distance(Character.WorldPosition, hull.WorldPosition); + float optimal = 1000; + float max = 3000; + // Prefer rooms that are far but not too far. + float distanceFactor = dist > optimal ? MathHelper.Lerp(1, 0, MathUtils.InverseLerp(optimal, max, dist)) : MathHelper.Lerp(0, 1, MathUtils.InverseLerp(0, optimal, dist)); + float waterFactor = 1; + if (checkWaterLevel) + { + waterFactor = AIParams.PatrolDry ? MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 100, hull.WaterPercentage)) : MathHelper.Lerp(0, 1, MathUtils.InverseLerp(0, 100, hull.WaterPercentage)); + } + weight *= distanceFactor * waterFactor; + hullWeights.Add(weight); + } + } + } + #endregion #region Attack @@ -1078,7 +1251,7 @@ namespace Barotrauma if (canAttack) { - if (AttackingLimb == null || _previousAiTarget != SelectedAiTarget) + if (AttackingLimb == null || !IsValidAttack(AttackingLimb, Character.GetAttackContexts(), SelectedAiTarget?.Entity as IDamageable)) { AttackingLimb = GetAttackLimb(attackWorldPos); } @@ -1262,6 +1435,8 @@ namespace Barotrauma State = AIState.Idle; return; } + + var pathSteering = SteeringManager as IndoorsSteeringManager; if (AttackingLimb != null && AttackingLimb.attack.Retreat) { @@ -1276,7 +1451,7 @@ namespace Barotrauma Vector2 offset = Character.SimPosition - steeringLimb.SimPosition; steerPos += offset; } - if (SteeringManager is IndoorsSteeringManager pathSteering) + if (pathSteering != null) { if (pathSteering.CurrentPath != null) { @@ -1298,243 +1473,295 @@ namespace Barotrauma } } } - // Steer towards the target if in the same room and swimming - if (Character.CurrentHull != null && ((Character.AnimController.InWater || pursue || !Character.AnimController.CanWalk) && - (targetCharacter != null && VisibleHulls.Contains(targetCharacter.CurrentHull)))) + // When pursuing, we don't want to pursue too close + float max = 300; + float margin = AttackingLimb != null ? Math.Min(AttackingLimb.attack.Range * 0.9f, max) : max; + if (!canAttack || distance > margin) { - Vector2 myPos = Character.AnimController.SimplePhysicsEnabled ? Character.SimPosition : steeringLimb.SimPosition; - SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(steerPos - myPos)); + // Steer towards the target if in the same room and swimming + if (Character.CurrentHull != null && ((Character.AnimController.InWater || pursue || !Character.AnimController.CanWalk) && + (targetCharacter != null && VisibleHulls.Contains(targetCharacter.CurrentHull)))) + { + Vector2 myPos = Character.AnimController.SimplePhysicsEnabled ? Character.SimPosition : steeringLimb.SimPosition; + SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(steerPos - myPos)); + } + else + { + pathSteering.SteeringSeek(steerPos, weight: 2, + minGapWidth: minGapSize, + startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (Character.CurrentHull == null), + nodeFilter: NodeFilter, + checkVisiblity: true); + + if (!pathSteering.IsPathDirty && pathSteering.CurrentPath.Unreachable) + { + State = AIState.Idle; + IgnoreTarget(SelectedAiTarget); + ResetAITarget(); + return; + } + } } else { - pathSteering.SteeringSeek(steerPos, 2, startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (Character.CurrentHull == null), checkVisiblity: true); - if (!pathSteering.IsPathDirty && pathSteering.CurrentPath.Unreachable) + if (AttackingLimb.attack.Ranged) { - State = AIState.Idle; - IgnoreTarget(SelectedAiTarget); - ResetAITarget(); - return; + float dir = Character.AnimController.Dir; + if (dir > 0 && attackWorldPos.X > AttackingLimb.WorldPosition.X + margin || dir < 0 && attackWorldPos.X < AttackingLimb.WorldPosition.X - margin) + { + SteeringManager.Reset(); + } + else + { + // Too close + UpdateFallBack(attackWorldPos, deltaTime, followThrough: false); + } + } + else + { + // Close enough + SteeringManager.Reset(); } } } else { - SteeringManager.SteeringSeek(steerPos, 5); + pathSteering.SteeringSeek(steerPos, weight: 5, minGapWidth: minGapSize, NodeFilter); } } else { - switch (selectedTargetingParams.AttackPattern) + // Sweeping and circling doesn't work well inside + if (Character.CurrentHull == null) { - case AttackPattern.Sweep: - if (selectedTargetingParams.SweepDistance > 0) - { - if (distance <= 0) + switch (selectedTargetingParams.AttackPattern) + { + case AttackPattern.Sweep: + if (selectedTargetingParams.SweepDistance > 0) { - distance = (attackWorldPos - WorldPosition).Length(); - } - float amplitude = MathHelper.Lerp(0, selectedTargetingParams.SweepStrength, MathUtils.InverseLerp(selectedTargetingParams.SweepDistance, 0, distance)); - if (amplitude > 0) - { - sweepTimer += deltaTime * selectedTargetingParams.SweepSpeed; - float sin = (float)Math.Sin(sweepTimer) * amplitude; - steerPos = MathUtils.RotatePointAroundTarget(attackSimPos, SimPosition, sin); - } - else - { - sweepTimer = Rand.Range(-1000, 1000) * selectedTargetingParams.SweepSpeed; - } - } - break; - case AttackPattern.Circle: - if (IsCoolDownRunning) { break; } - if (IsAttackRunning && CirclePhase != CirclePhase.Strike) { break; } - if (selectedTargetingParams == null) { break; } - var targetSub = SelectedAiTarget.Entity?.Submarine; - if (targetSub == null) { break; } - float subSize = Math.Max(targetSub.Borders.Width, targetSub.Borders.Height) / 2; - float sqrDistToSub = Vector2.DistanceSquared(WorldPosition, targetSub.WorldPosition); - switch (CirclePhase) - { - case CirclePhase.Start: - currentAttackIntensity = MathUtils.InverseLerp(AIParams.StartAggression, AIParams.MaxAggression, aggressionIntensity * Rand.Range(0.9f, 1.1f)); - inverseDir = false; - circleDir = GetDirFromHeadingInRadius(); - circleRotation = 0; - strikeTimer = 0; - blockCheckTimer = 0; - breakCircling = false; - float minRotationSpeed = 0.01f * selectedTargetingParams.CircleRotationSpeed; - float maxRotationSpeed = 0.5f * selectedTargetingParams.CircleRotationSpeed; - float minFallBackDistance = selectedTargetingParams.CircleStartDistance * 0.5f; - float maxFallBackDistance = selectedTargetingParams.CircleStartDistance; - // The lower the rotation speed, the slower the progression. Also the distance to the target stays longer. - // So basically if the value is higher, the creature will strike the sub more quickly and with more precision. - circleRotationSpeed = MathHelper.Lerp(minRotationSpeed, maxRotationSpeed, currentAttackIntensity * Rand.Range(0.9f, 1.1f)); - circleFallbackDistance = MathHelper.Lerp(maxFallBackDistance, minFallBackDistance, currentAttackIntensity * Rand.Range(0.9f, 1.1f)); - circleOffset = Rand.Vector(MathHelper.Lerp(selectedTargetingParams.CircleMaxRandomOffset, 0, currentAttackIntensity * Rand.Range(0.9f, 1.1f))); - canAttack = false; - aggressionIntensity = Math.Clamp(aggressionIntensity, AIParams.StartAggression, AIParams.MaxAggression); - if (targetSub.Borders.Width < 1000) + if (distance <= 0) { - breakCircling = true; - CirclePhase = CirclePhase.CloseIn; + distance = (attackWorldPos - WorldPosition).Length(); } - else if (sqrDistToSub > MathUtils.Pow2(subSize + selectedTargetingParams.CircleStartDistance)) + float amplitude = MathHelper.Lerp(0, selectedTargetingParams.SweepStrength, MathUtils.InverseLerp(selectedTargetingParams.SweepDistance, 0, distance)); + if (amplitude > 0) { - CirclePhase = CirclePhase.CloseIn; - } - else if (sqrDistToSub < MathUtils.Pow2(subSize + circleFallbackDistance)) - { - CirclePhase = CirclePhase.FallBack; + sweepTimer += deltaTime * selectedTargetingParams.SweepSpeed; + float sin = (float)Math.Sin(sweepTimer) * amplitude; + steerPos = MathUtils.RotatePointAroundTarget(attackSimPos, SimPosition, sin); } else { - CirclePhase = CirclePhase.Advance; + sweepTimer = Rand.Range(-1000, 1000) * selectedTargetingParams.SweepSpeed; } - break; - case CirclePhase.CloseIn: - if (AttackingLimb != null && distance > 0 && distance < AttackingLimb.attack.Range * GetStrikeDistanceMultiplier(targetSub.Velocity)) - { - strikeTimer = AttackingLimb.attack.CoolDown; - CirclePhase = CirclePhase.Strike; - } - else if (!breakCircling && sqrDistToSub <= MathUtils.Pow2(subSize + selectedTargetingParams.CircleStartDistance / 2) && targetSub.Velocity.LengthSquared() <= MathUtils.Pow2(GetTargetMaxSpeed())) - { - CirclePhase = CirclePhase.Advance; - } - canAttack = false; - break; - case CirclePhase.FallBack: - bool isBlocked = !UpdateFallBack(attackWorldPos, deltaTime, followThrough: false, checkBlocking: true); - if (isBlocked || sqrDistToSub > MathUtils.Pow2(subSize + circleFallbackDistance)) - { - CirclePhase = CirclePhase.Advance; + } + break; + case AttackPattern.Circle: + if (IsCoolDownRunning) { break; } + if (IsAttackRunning && CirclePhase != CirclePhase.Strike) { break; } + if (selectedTargetingParams == null) { break; } + var targetSub = SelectedAiTarget.Entity?.Submarine; + if (targetSub == null) { break; } + float subSize = Math.Max(targetSub.Borders.Width, targetSub.Borders.Height) / 2; + float sqrDistToSub = Vector2.DistanceSquared(WorldPosition, targetSub.WorldPosition); + switch (CirclePhase) + { + case CirclePhase.Start: + currentAttackIntensity = MathUtils.InverseLerp(AIParams.StartAggression, AIParams.MaxAggression, aggressionIntensity * Rand.Range(0.9f, 1.1f)); + inverseDir = false; + circleDir = GetDirFromHeadingInRadius(); + circleRotation = 0; + strikeTimer = 0; + blockCheckTimer = 0; + breakCircling = false; + float minRotationSpeed = 0.01f * selectedTargetingParams.CircleRotationSpeed; + float maxRotationSpeed = 0.5f * selectedTargetingParams.CircleRotationSpeed; + float minFallBackDistance = selectedTargetingParams.CircleStartDistance * 0.5f; + float maxFallBackDistance = selectedTargetingParams.CircleStartDistance; + // The lower the rotation speed, the slower the progression. Also the distance to the target stays longer. + // So basically if the value is higher, the creature will strike the sub more quickly and with more precision. + circleRotationSpeed = MathHelper.Lerp(minRotationSpeed, maxRotationSpeed, currentAttackIntensity * Rand.Range(0.9f, 1.1f)); + circleFallbackDistance = MathHelper.Lerp(maxFallBackDistance, minFallBackDistance, currentAttackIntensity * Rand.Range(0.9f, 1.1f)); + circleOffset = Rand.Vector(MathHelper.Lerp(selectedTargetingParams.CircleMaxRandomOffset, 0, currentAttackIntensity * Rand.Range(0.9f, 1.1f))); + canAttack = false; + aggressionIntensity = Math.Clamp(aggressionIntensity, AIParams.StartAggression, AIParams.MaxAggression); + if (targetSub.Borders.Width < 1000) + { + breakCircling = true; + CirclePhase = CirclePhase.CloseIn; + } + else if (sqrDistToSub > MathUtils.Pow2(subSize + selectedTargetingParams.CircleStartDistance)) + { + CirclePhase = CirclePhase.CloseIn; + } + else if (sqrDistToSub < MathUtils.Pow2(subSize + circleFallbackDistance)) + { + CirclePhase = CirclePhase.FallBack; + } + else + { + CirclePhase = CirclePhase.Advance; + } break; - } - return; - case CirclePhase.Advance: - Vector2 subSpeed = targetSub.Velocity; - float requiredDistMultiplier = 1; - // If the target sub is moving fast, just steer towards the target until close enough to strike - if (breakCircling || subSpeed.LengthSquared() > MathUtils.Pow2(GetTargetMaxSpeed()) || sqrDistToSub > MathUtils.Pow2(subSize + selectedTargetingParams.CircleStartDistance * 1.2f)) - { - CirclePhase = CirclePhase.CloseIn; - } - else - { - circleRotation += deltaTime * circleRotationSpeed * circleDir; - if (circleRotation < -360) + case CirclePhase.CloseIn: + if (AttackingLimb != null && distance > 0 && distance < AttackingLimb.attack.Range * GetStrikeDistanceMultiplier(targetSub.Velocity)) { - circleRotation += 360; + strikeTimer = AttackingLimb.attack.CoolDown; + CirclePhase = CirclePhase.Strike; } - else if (circleRotation > 360) + else if (!breakCircling && sqrDistToSub <= MathUtils.Pow2(subSize + selectedTargetingParams.CircleStartDistance / 2) && targetSub.Velocity.LengthSquared() <= MathUtils.Pow2(GetTargetMaxSpeed())) { - circleRotation -= 360; + CirclePhase = CirclePhase.Advance; } - Vector2 targetPos = attackSimPos + circleOffset; - if (Vector2.DistanceSquared(SimPosition, targetPos) < 100) + canAttack = false; + break; + case CirclePhase.FallBack: + bool isBlocked = !UpdateFallBack(attackWorldPos, deltaTime, followThrough: false, checkBlocking: true); + if (isBlocked || sqrDistToSub > MathUtils.Pow2(subSize + circleFallbackDistance)) { - // Too close to the target point - // When the offset position is outside of the sub it happens that the creature sometimes reaches the target point, - // which makes it continue circling around the point (as supposed) - // But when there is some offset and the offset is too near, this is not what we want. - if (AttackingLimb != null && sqrDistToSub < MathUtils.Pow2(subSize + circleFallbackDistance)) - { - CirclePhase = CirclePhase.Strike; - strikeTimer = AttackingLimb.attack.CoolDown; - } - else - { - CirclePhase = CirclePhase.Start; - } + CirclePhase = CirclePhase.Advance; break; } - steerPos = MathUtils.RotatePointAroundTarget(SimPosition, targetPos, circleRotation); - requiredDistMultiplier = GetStrikeDistanceMultiplier(subSpeed); - if (IsBlocked(deltaTime, steerPos)) + return; + case CirclePhase.Advance: + Vector2 subSpeed = targetSub.Velocity; + float requiredDistMultiplier = 1; + // If the target sub is moving fast, just steer towards the target until close enough to strike + if (breakCircling || subSpeed.LengthSquared() > MathUtils.Pow2(GetTargetMaxSpeed()) || sqrDistToSub > MathUtils.Pow2(subSize + selectedTargetingParams.CircleStartDistance * 1.2f)) { - if (!inverseDir) + CirclePhase = CirclePhase.CloseIn; + } + else + { + circleRotation += deltaTime * circleRotationSpeed * circleDir; + if (circleRotation < -360) { - // First try changing the direction - circleDir = -circleDir; - inverseDir = true; + circleRotation += 360; } - else if (circleRotationSpeed < 1) + else if (circleRotation > 360) { - // Then try increasing the rotation speed to change the movement curve - circleRotationSpeed *= 1.1f; + circleRotation -= 360; } - else if (circleOffset.LengthSquared() > 0.1f) + Vector2 targetPos = attackSimPos + circleOffset; + if (Vector2.DistanceSquared(SimPosition, targetPos) < 100) { - // Then try removing the offset - circleOffset = Vector2.Zero; + // Too close to the target point + // When the offset position is outside of the sub it happens that the creature sometimes reaches the target point, + // which makes it continue circling around the point (as supposed) + // But when there is some offset and the offset is too near, this is not what we want. + if (AttackingLimb != null && sqrDistToSub < MathUtils.Pow2(subSize + circleFallbackDistance)) + { + CirclePhase = CirclePhase.Strike; + strikeTimer = AttackingLimb.attack.CoolDown; + } + else + { + CirclePhase = CirclePhase.Start; + } + break; } - else + steerPos = MathUtils.RotatePointAroundTarget(SimPosition, targetPos, circleRotation); + requiredDistMultiplier = GetStrikeDistanceMultiplier(subSpeed); + if (IsBlocked(deltaTime, steerPos)) { - // If we still fail, just steer towards the target - breakCircling = true; + if (!inverseDir) + { + // First try changing the direction + circleDir = -circleDir; + inverseDir = true; + } + else if (circleRotationSpeed < 1) + { + // Then try increasing the rotation speed to change the movement curve + circleRotationSpeed *= 1.1f; + } + else if (circleOffset.LengthSquared() > 0.1f) + { + // Then try removing the offset + circleOffset = Vector2.Zero; + } + else + { + // If we still fail, just steer towards the target + breakCircling = true; + } } } - } - if (AttackingLimb != null && distance > 0 && distance < AttackingLimb.attack.Range * requiredDistMultiplier && IsFacing(margin: MathHelper.Lerp(0.5f, 0.9f, currentAttackIntensity))) - { - strikeTimer = AttackingLimb.attack.CoolDown; - CirclePhase = CirclePhase.Strike; - } - canAttack = false; - break; - case CirclePhase.Strike: - strikeTimer -= deltaTime; - // just continue the movement forward to make it possible to evade the attack - steerPos = SimPosition + Steering; - if (strikeTimer <= 0) - { - CirclePhase = CirclePhase.Start; - aggressionIntensity += AIParams.AggressionCumulation; - } - break; - } - break; - - bool IsFacing(float margin) - { - float offset = steeringLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2; - Vector2 forward = VectorExtensions.Forward(steeringLimb.body.TransformedRotation - offset * Character.AnimController.Dir); - return Vector2.Dot(Vector2.Normalize(attackWorldPos - WorldPosition), forward) > margin; - } - - float GetStrikeDistanceMultiplier(Vector2 subSpeed) - { - float requiredDistMultiplier = 2; - bool isHeading = Steering != null && Vector2.Dot(Vector2.Normalize(attackWorldPos - WorldPosition), Vector2.Normalize(Steering)) > 0.9f; - if (isHeading) - { - requiredDistMultiplier = selectedTargetingParams.CircleStrikeDistanceMultiplier; - float subSpeedHorizontal = Math.Abs(subSpeed.X); - if (subSpeedHorizontal > 1) - { - // Reduce the required distance if the target is moving. - requiredDistMultiplier -= MathHelper.Lerp(0, Math.Max(selectedTargetingParams.CircleStrikeDistanceMultiplier - 1, 1), Math.Clamp(subSpeedHorizontal / 10, 0, 1)); - if (requiredDistMultiplier < 2) + if (AttackingLimb != null && distance > 0 && distance < AttackingLimb.attack.Range * requiredDistMultiplier && IsFacing(margin: MathHelper.Lerp(0.5f, 0.9f, currentAttackIntensity))) { - requiredDistMultiplier = 2; + strikeTimer = AttackingLimb.attack.CoolDown; + CirclePhase = CirclePhase.Strike; } - } + canAttack = false; + break; + case CirclePhase.Strike: + strikeTimer -= deltaTime; + // just continue the movement forward to make it possible to evade the attack + steerPos = SimPosition + Steering; + if (strikeTimer <= 0) + { + CirclePhase = CirclePhase.Start; + aggressionIntensity += AIParams.AggressionCumulation; + } + break; } - return requiredDistMultiplier; - } + break; - float GetDirFromHeadingInRadius() - { - Vector2 heading = VectorExtensions.Forward(Character.AnimController.Collider.Rotation); - float angle = MathUtils.VectorToAngle(heading); - return angle > MathHelper.Pi || angle < -MathHelper.Pi ? -1 : 1; - } + bool IsFacing(float margin) + { + float offset = steeringLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2; + Vector2 forward = VectorExtensions.Forward(steeringLimb.body.TransformedRotation - offset * Character.AnimController.Dir); + return Vector2.Dot(Vector2.Normalize(attackWorldPos - WorldPosition), forward) > margin; + } - float GetTargetMaxSpeed() => Character.ApplyTemporarySpeedLimits(Character.AnimController.CurrentSwimParams.MovementSpeed * 0.3f); + float GetStrikeDistanceMultiplier(Vector2 subSpeed) + { + float requiredDistMultiplier = 2; + bool isHeading = Steering != null && Vector2.Dot(Vector2.Normalize(attackWorldPos - WorldPosition), Vector2.Normalize(Steering)) > 0.9f; + if (isHeading) + { + requiredDistMultiplier = selectedTargetingParams.CircleStrikeDistanceMultiplier; + float subSpeedHorizontal = Math.Abs(subSpeed.X); + if (subSpeedHorizontal > 1) + { + // Reduce the required distance if the target is moving. + requiredDistMultiplier -= MathHelper.Lerp(0, Math.Max(selectedTargetingParams.CircleStrikeDistanceMultiplier - 1, 1), Math.Clamp(subSpeedHorizontal / 10, 0, 1)); + if (requiredDistMultiplier < 2) + { + requiredDistMultiplier = 2; + } + } + } + return requiredDistMultiplier; + } + + float GetDirFromHeadingInRadius() + { + Vector2 heading = VectorExtensions.Forward(Character.AnimController.Collider.Rotation); + float angle = MathUtils.VectorToAngle(heading); + return angle > MathHelper.Pi || angle < -MathHelper.Pi ? -1 : 1; + } + + float GetTargetMaxSpeed() => Character.ApplyTemporarySpeedLimits(Character.AnimController.CurrentSwimParams.MovementSpeed * 0.3f); + } + } + + if (!canAttack || distance > Math.Min(AttackingLimb.attack.Range * 0.9f, 100)) + { + if (pathSteering != null) + { + pathSteering.SteeringSeek(steerPos, weight: 10, minGapWidth: minGapSize, NodeFilter); + } + else + { + SteeringManager.SteeringSeek(steerPos, 10); + } + } + else if (AttackingLimb.attack.Ranged) + { + // Too close + UpdateFallBack(attackWorldPos, deltaTime, followThrough: false); } - SteeringManager.SteeringSeek(steerPos, 10); if (SelectedAiTarget?.Entity is Character c && c.Submarine == null || distance == 0 || distance > ConvertUnits.ToDisplayUnits(avoidLookAheadDistance * 2)) { SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 30); @@ -1554,12 +1781,39 @@ namespace Barotrauma } } + private bool IsValidAttack(Limb attackingLimb, IEnumerable currentContexts, IDamageable target) + { + if (attackingLimb == null) { return false; } + if (target == null) { return false; } + var attack = attackingLimb.attack; + if (attack == null) { return false; } + if (attack.CoolDownTimer > 0) { return false; } + if (!attack.IsValidContext(currentContexts)) { return false; } + if (!attack.IsValidTarget(target)) { return false; } + if (target is ISerializableEntity se && target is Character) + { + if (attack.Conditionals.Any(c => !c.Matches(se))) { return false; } + } + if (attack.Conditionals.Any(c => c.TargetSelf && !c.Matches(Character))) { return false; } + if (attack.Ranged) + { + // Check that is approximately facing the target + Vector2 attackLimbPos = Character.AnimController.SimplePhysicsEnabled ? Character.WorldPosition : attackingLimb.WorldPosition; + Vector2 toTarget = attackWorldPos - attackLimbPos; + float offset = attackingLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2; + Vector2 forward = VectorExtensions.Forward(attackingLimb.body.TransformedRotation - offset * Character.AnimController.Dir); + float angle = VectorExtensions.Angle(forward, toTarget); + if (angle > MathHelper.ToRadians(attack.RequiredAngle)) { return false; } + } + return true; + } + private readonly List attackLimbs = new List(); private readonly List weights = new List(); private Limb GetAttackLimb(Vector2 attackWorldPos, Limb ignoredLimb = null) { var currentContexts = Character.GetAttackContexts(); - Entity target = wallTarget != null ? wallTarget.Structure : SelectedAiTarget?.Entity; + IDamageable target = wallTarget != null ? wallTarget.Structure : SelectedAiTarget?.Entity as IDamageable; if (target == null) { return null; } Limb selectedLimb = null; float currentPriority = -1; @@ -1567,28 +1821,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; } - if (!attack.IsValidContext(currentContexts)) { continue; } - if (!attack.IsValidTarget(target as IDamageable)) { continue; } - if (target is ISerializableEntity se && target is Character) - { - 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 (!IsValidAttack(limb, currentContexts, target)) { continue; } if (AIParams.RandomAttack) { attackLimbs.Add(limb); @@ -1632,6 +1865,10 @@ namespace Barotrauma Character.AnimController.ReleaseStuckLimbs(); LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 1); if (attacker == null || attacker.AiTarget == null || attacker.Removed || attacker.IsDead) { return; } + if (Character.Params.CanInteract) + { + ReleaseDragTargets(); + } bool isFriendly = Character.IsFriendly(attacker); if (wasLatched) { @@ -1643,7 +1880,6 @@ namespace Barotrauma } return; } - if (State == AIState.Flee) { if (!isFriendly) @@ -1746,6 +1982,26 @@ namespace Barotrauma } } + private Item GetEquippedItem(Limb limb) + { + InvSlotType GetInvSlotForLimb() + { + return limb.type switch + { + LimbType.RightHand => InvSlotType.RightHand, + LimbType.LeftHand => InvSlotType.LeftHand, + LimbType.Head => InvSlotType.Head, + _ => InvSlotType.None, + }; + } + var slot = GetInvSlotForLimb(); + if (slot != InvSlotType.None) + { + return Character.Inventory.GetItemInLimbSlot(slot); + } + return null; + } + // 10 dmg, 100 health -> 0.1 private float GetRelativeDamage(float dmg, float vitality) => dmg / Math.Max(vitality, 1.0f); @@ -1767,6 +2023,24 @@ namespace Barotrauma IDamageable damageTarget = wallTarget != null ? wallTarget.Structure : SelectedAiTarget.Entity as IDamageable; if (damageTarget != null) { + if (Character.Params.CanInteract && Character.Inventory != null) + { + // Use equipped items (weapons) + Item item = GetEquippedItem(attackingLimb); + if (item != null) + { + if (item.RequireAimToUse) + { + if (!Aim(deltaTime, damageTarget as ISpatialEntity, item)) + { + // Valid target, but can't shoot -> return true so that it will not be ignored. + return true; + } + } + Character.SetInput(item.IsShootable ? InputType.Shoot : InputType.Use, false, true); + item.Use(deltaTime, Character); + } + } //simulate attack input to get the character to attack client-side Character.SetInput(InputType.Attack, true, true); if (!ActiveAttack.IsRunning) @@ -1788,8 +2062,9 @@ namespace Barotrauma if (attackingLimb.UpdateAttack(deltaTime, attackSimPos, damageTarget, out AttackResult attackResult, distance, targetLimb)) { - if (damageTarget.Health > 0 && attackResult.Damage > 0) + if (attackingLimb.attack.CoolDownTimer > 0) { + SetAimTimer(); // Managed to hit a living/non-destroyed target. Increase the priority more if the target is low in health -> dies easily/soon float greed = AIParams.AggressionGreed; if (!(damageTarget is Character)) @@ -1799,10 +2074,28 @@ namespace Barotrauma } selectedTargetMemory.Priority += GetRelativeDamage(attackResult.Damage, damageTarget.Health) * greed; } - else + if (LatchOntoAI != null && SelectedAiTarget.Entity is Character targetCharacter) { - selectedTargetMemory.Priority -= Math.Max(selectedTargetMemory.Priority / 2, 1); - return selectedTargetMemory.Priority > 1; + LatchOntoAI.SetAttachTarget(targetCharacter); + } + if (!attackingLimb.attack.Ranged) + { + if (damageTarget.Health > 0 && attackResult.Damage > 0) + { + // Managed to hit a living/non-destroyed target. Increase the priority more if the target is low in health -> dies easily/soon + float greed = AIParams.AggressionGreed; + if (!(damageTarget is Character)) + { + // Halve the greed for attacking non-characters. + greed /= 2; + } + selectedTargetMemory.Priority += GetRelativeDamage(attackResult.Damage, damageTarget.Health) * greed; + } + else + { + selectedTargetMemory.Priority -= Math.Max(selectedTargetMemory.Priority / 2, 1); + return selectedTargetMemory.Priority > 1; + } } } return true; @@ -1810,6 +2103,64 @@ namespace Barotrauma return false; } + private float aimTimer; + private float visibilityCheckTimer; + private bool canSeeTarget; + private bool Aim(float deltaTime, ISpatialEntity target, Item weapon) + { + if (target == null || weapon == null) { return false; } + Character.CursorPosition = target.WorldPosition; + if (Character.Submarine != null) + { + Character.CursorPosition -= Character.Submarine.Position; + } + visibilityCheckTimer -= deltaTime; + if (visibilityCheckTimer <= 0.0f) + { + canSeeTarget = Character.CanSeeTarget(target); + visibilityCheckTimer = 0.2f; + } + if (!canSeeTarget) + { + SetAimTimer(); + return false; + } + Character.SetInput(InputType.Aim, false, true); + if (aimTimer > 0) + { + aimTimer -= deltaTime; + return false; + } + Vector2 toTarget = target.WorldPosition - weapon.WorldPosition; + float angle = VectorExtensions.Angle(VectorExtensions.Forward(weapon.body.TransformedRotation), toTarget); + float distanceFactor = MathHelper.Lerp(1.0f, 0.1f, MathUtils.InverseLerp(100, 1000, toTarget.Length())); + float margin = MathHelper.PiOver4 * distanceFactor; + if (angle < margin) + { + var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel; + var pickedBody = Submarine.PickBody(weapon.SimPosition, target.SimPosition, myBodies, collisionCategories, allowInsideFixture: true); + if (pickedBody != null) + { + Character t = null; + if (pickedBody.UserData is Character c) + { + t = c; + } + else if (pickedBody.UserData is Limb limb) + { + t = limb.character; + } + if (t != null && (t == target || !Character.IsFriendly(t))) + { + return true; + } + } + } + return false; + } + + private void SetAimTimer(float timer = 1.5f) => aimTimer = timer * Rand.Range(0.75f, 1.25f); + private readonly float blockCheckInterval = 0.1f; private float blockCheckTimer; private bool isBlocked; @@ -1948,7 +2299,7 @@ namespace Barotrauma else { // Use path finding - SteeringManager.SteeringSeek(Character.GetRelativeSimPosition(SelectedAiTarget.Entity), 2); + PathSteering.SteeringSeek(Character.GetRelativeSimPosition(SelectedAiTarget.Entity), weight: 2, minGapWidth: minGapSize, NodeFilter); if (!PathSteering.IsPathDirty && PathSteering.CurrentPath.Unreachable) { // Can't reach @@ -1984,7 +2335,7 @@ namespace Barotrauma foreach (AITarget aiTarget in AITarget.List) { - if (!aiTarget.Enabled) { continue; } + if (aiTarget.InDetectable) { continue; } if (aiTarget.Entity == null) { continue; } if (ignoredTargets.Contains(aiTarget)) { continue; } if (Level.Loaded != null && aiTarget.WorldPosition.Y > Level.Loaded.Size.Y) @@ -2307,11 +2658,24 @@ namespace Barotrauma continue; } } - if (aiTarget.Entity is Item targetItem && targetParams.IgnoreContained && targetItem.ParentInventory != null) { continue; } + if (aiTarget.Entity is Item targetItem) + { + if (targetParams.IgnoreContained && targetItem.ParentInventory != null) { continue; } + if (targetParams.State == AIState.FleeTo) + { + float target = targetParams.Threshold; + if (targetParams.ThresholdMin > 0 && targetParams.ThresholdMax > 0) + { + target = selectedTargetingParams == targetParams ? targetParams.ThresholdMax : targetParams.ThresholdMin; + } + if (character.HealthPercentage > target) + { + continue; + } + } + } valueModifier *= targetParams.Priority; - if (valueModifier == 0.0f) { continue; } - if (targetingTag != "decoy") { if (SwarmBehavior != null && SwarmBehavior.Members.Any()) @@ -2347,15 +2711,25 @@ namespace Barotrauma } if (!CanPerceive(aiTarget, dist)) { continue; } + if (SelectedAiTarget == aiTarget) + { + // Stick to the current target + valueModifier *= 1.1f; + } + //if the target is very close, the distance doesn't make much difference // -> just ignore the distance and attack whatever has the highest priority dist = Math.Max(dist, 100.0f); AITargetMemory targetMemory = GetTargetMemory(aiTarget, addIfNotFound: true); - if (Character.CurrentHull != null && Math.Abs(toTarget.Y) > Character.CurrentHull.Size.Y) + if (Character.Submarine != null && !Character.Submarine.Info.IsRuin && Character.CurrentHull != null) { - // Inside the sub, treat objects that are up or down, as they were farther away. - dist *= 3; + float diff = Math.Abs(toTarget.Y) - Character.CurrentHull.Size.Y; + if (diff > 0) + { + // Inside the sub, treat objects that are up or down, as they were farther away. + dist *= MathHelper.Clamp(diff / 100, 2, 3); + } } if (targetParams.AttackPattern == AttackPattern.Circle) @@ -2375,6 +2749,12 @@ namespace Barotrauma } } + if (targetCharacter != null && Character.CurrentHull != null && Character.CurrentHull == targetCharacter.CurrentHull) + { + // In the same room with the target character + dist /= 2; + } + // Don't target characters that are outside of the allowed zone, unless chasing or escaping. switch (targetParams.State) { @@ -2887,7 +3267,7 @@ namespace Barotrauma private void ResetParams(CharacterParams.TargetParams targetParams) { targetParams?.Reset(); - if (selectedTargetingParams == targetParams || State == AIState.Idle) + if (selectedTargetingParams == targetParams || State == AIState.Idle || State == AIState.Patrol) { ResetAITarget(); State = AIState.Idle; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index b3f381c2d..5c2c5d3e8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -1036,16 +1036,19 @@ namespace Barotrauma } if (previousAttackResults.ContainsKey(attacker)) { - foreach (Affliction newAffliction in attackResult.Afflictions) + if (attackResult.Afflictions != null) { - var matchingAffliction = previousAttackResults[attacker].Afflictions.Find(a => a.Prefab == newAffliction.Prefab && a.Source == newAffliction.Source); - if (matchingAffliction == null) + foreach (Affliction newAffliction in attackResult.Afflictions) { - previousAttackResults[attacker].Afflictions.Add(newAffliction); - } - else - { - matchingAffliction.Strength += newAffliction.Strength; + var matchingAffliction = previousAttackResults[attacker].Afflictions.Find(a => a.Prefab == newAffliction.Prefab && a.Source == newAffliction.Source); + if (matchingAffliction == null) + { + previousAttackResults[attacker].Afflictions.Add(newAffliction); + } + else + { + matchingAffliction.Strength += newAffliction.Strength; + } } } previousAttackResults[attacker] = new AttackResult(previousAttackResults[attacker].Afflictions, previousAttackResults[attacker].HitLimb); @@ -1062,9 +1065,12 @@ namespace Barotrauma float realDamage = attackResult.Damage; // including poisons etc float totalDamage = realDamage; - foreach (Affliction affliction in attackResult.Afflictions) + if (attackResult.Afflictions != null) { - totalDamage -= affliction.Prefab.KarmaChangeOnApplied * affliction.Strength; + foreach (Affliction affliction in attackResult.Afflictions) + { + totalDamage -= affliction.Prefab.KarmaChangeOnApplied * affliction.Strength; + } } if (totalDamage <= 0.01f) { return; } if (Character.IsBot) @@ -1255,7 +1261,7 @@ namespace Barotrauma // Already targeting the attacker -> treat as a more serious threat. cumulativeDamage *= 2; } - if (attackResult.Afflictions.Any(a => a is AfflictionHusk)) + if (attackResult.Afflictions != null && attackResult.Afflictions.Any(a => a is AfflictionHusk)) { cumulativeDamage = 100; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs index ef353d352..eebf7b0ec 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs @@ -2,7 +2,6 @@ using Microsoft.Xna.Framework; using System; using System.Linq; -using Barotrauma.Extensions; using FarseerPhysics; namespace Barotrauma @@ -15,6 +14,11 @@ namespace Barotrauma private bool canOpenDoors; public bool CanBreakDoors { get; set; } + private bool ShouldBreakDoor(Door door) => + CanBreakDoors && + !door.Item.Indestructible && !door.Item.InvulnerableToDamage && + (door.Item.Submarine == null || door.Item.Submarine.TeamID != character.TeamID); + private Character character; private Vector2 currentTarget; @@ -23,7 +27,7 @@ namespace Barotrauma private float buttonPressCooldown; - const float ButtonPressInterval = 0.5f; + const float ButtonPressInterval = 0.25f; public SteeringPath CurrentPath { @@ -111,9 +115,9 @@ namespace Barotrauma IsPathDirty = true; } - public void SteeringSeek(Vector2 target, float weight, Func startNodeFilter = null, Func endNodeFilter = null, Func nodeFilter = null, bool checkVisiblity = true) + public void SteeringSeek(Vector2 target, float weight, float minGapWidth = 0, Func startNodeFilter = null, Func endNodeFilter = null, Func nodeFilter = null, bool checkVisiblity = true) { - steering += CalculateSteeringSeek(target, weight, startNodeFilter, endNodeFilter, nodeFilter, checkVisiblity); + steering += CalculateSteeringSeek(target, weight, minGapWidth, startNodeFilter, endNodeFilter, nodeFilter, checkVisiblity); } /// @@ -158,7 +162,7 @@ namespace Barotrauma } } - private Vector2 CalculateSteeringSeek(Vector2 target, float weight, Func startNodeFilter = null, Func endNodeFilter = null, Func nodeFilter = null, bool checkVisibility = true) + private Vector2 CalculateSteeringSeek(Vector2 target, float weight, float minGapSize = 0, Func startNodeFilter = null, Func endNodeFilter = null, Func nodeFilter = null, bool checkVisibility = true) { bool needsNewPath = currentPath == null || currentPath.Unreachable; if (!needsNewPath && character.Submarine != null && character.Params.PathFinderPriority > 0.5f) @@ -200,7 +204,7 @@ 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); + var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", minGapSize, startNodeFilter, endNodeFilter, nodeFilter, checkVisibility: checkVisibility); bool useNewPath = needsNewPath || currentPath == null || currentPath.CurrentNode == null || character.Submarine != null && findPathTimer < -1 && Math.Abs(character.AnimController.TargetMovement.X) <= 0; if (!useNewPath && currentPath != null && currentPath.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable) { @@ -241,6 +245,10 @@ namespace Barotrauma } if (useNewPath) { + if (currentPath != null) + { + CheckDoorsInPath(); + } currentPath = newPath; } float priority = MathHelper.Lerp(3, 1, character.Params.PathFinderPriority); @@ -306,10 +314,12 @@ namespace Barotrauma pos2 -= CurrentPath.Nodes.Last().Submarine.SimPosition; } return currentTarget - pos2; - } - if (canOpenDoors && !character.LockHands && buttonPressCooldown <= 0.0f) + } + bool doorsChecked = false; + if (!character.LockHands && buttonPressCooldown <= 0.0f) { CheckDoorsInPath(); + doorsChecked = true; } Vector2 pos = host.SimPosition; if (character != null && CurrentPath.CurrentNode?.Submarine != null) @@ -394,7 +404,7 @@ namespace Barotrauma } if (isAboveFloor || nextLadderSameAsCurrent) { - currentPath.SkipToNextNode(); + NextNode(!doorsChecked); } } else if (nextLadder != null) @@ -404,7 +414,7 @@ namespace Barotrauma //e.g. no point in going down to reach the starting point of a path when we could go directly to the one above if (Math.Sign(currentPath.CurrentNode.WorldPosition.Y - character.WorldPosition.Y) != Math.Sign(currentPath.NextNode.WorldPosition.Y - character.WorldPosition.Y)) { - currentPath.SkipToNextNode(); + NextNode(!doorsChecked); } } return diff; @@ -426,7 +436,7 @@ namespace Barotrauma float distance = horizontalDistance + verticalDistance; if (ConvertUnits.ToSimUnits(distance) < targetDistance) { - currentPath.SkipToNextNode(); + NextNode(!doorsChecked); } } } @@ -451,7 +461,7 @@ namespace Barotrauma float targetDistance = Math.Max(colliderSize.X / 2 * margin, minWidth / 2); if (horizontalDistance < targetDistance && isAboveFeet && isNotTooHigh && (door == null || door.CanBeTraversed)) { - currentPath.SkipToNextNode(); + NextNode(!doorsChecked); } } if (currentPath.CurrentNode == null) @@ -461,28 +471,51 @@ namespace Barotrauma return currentPath.CurrentNode.SimPosition - pos; } + private void NextNode(bool checkDoors) + { + if (checkDoors) + { + CheckDoorsInPath(); + } + currentPath.SkipToNextNode(); + } + private bool CanAccessDoor(Door door, Func buttonFilter = null) { - if (door.IsOpen || door.IsBroken) { return true; } - if (!door.Item.IsInteractable(character)) { return false; } - if (!CanBreakDoors) + if (door.IsBroken) { return true; } + if (!door.IsOpen) { - if (door.IsStuck || door.IsJammed) { return false; } - if (!canOpenDoors || character.LockHands) { return false; } + if (!door.Item.IsInteractable(character)) { return false; } + if (!ShouldBreakDoor(door)) + { + if (door.IsStuck || door.IsJammed) { return false; } + if (!canOpenDoors || character.LockHands) { return false; } + } } if (door.HasIntegratedButtons) { - return door.HasAccess(character) || CanBreakDoors; + return door.IsOpen || door.HasAccess(character) || ShouldBreakDoor(door); } else { - return door.Item.GetConnectedComponents(true).Any(b => b.HasAccess(character) && (buttonFilter == null || buttonFilter(b))) || CanBreakDoors; + // We'll want this to run each time, because the delegate is used to find a valid button component. + bool canAccessButtons = door.Item.GetConnectedComponents(true).Any(b => b.HasAccess(character) && (buttonFilter == null || buttonFilter(b))); + return canAccessButtons || door.IsOpen || ShouldBreakDoor(door); } } + private Vector2 GetColliderSize() => ConvertUnits.ToDisplayUnits(character.AnimController.Collider.GetSize()); + + private float GetColliderLength() + { + Vector2 colliderSize = character.AnimController.Collider.GetSize(); + return ConvertUnits.ToDisplayUnits(Math.Max(colliderSize.X, colliderSize.Y)); + } + private void CheckDoorsInPath() { - for (int i = 0; i < 2; i++) + if (!canOpenDoors) { return; } + for (int i = 0; i < 5; i++) { WayPoint currentWaypoint = null; WayPoint nextWaypoint = null; @@ -493,17 +526,21 @@ namespace Barotrauma { door = currentPath.Nodes.First().ConnectedDoor; shouldBeOpen = door != null; + if (i > 0) { break; } } else { - if (i == 0) + bool closeDoors = character.IsBot && character.IsInFriendlySub || character.Params.AI != null && character.Params.AI.KeepDoorsClosed; + if (i == 0 || !closeDoors) { currentWaypoint = currentPath.CurrentNode; nextWaypoint = currentPath.NextNode; } else { - currentWaypoint = currentPath.PrevNode; + int previousIndex = currentPath.CurrentIndex - i; + if (previousIndex < 0) { break; } + currentWaypoint = currentPath.Nodes[previousIndex]; nextWaypoint = currentPath.CurrentNode; } if (currentWaypoint?.ConnectedDoor == null) { continue; } @@ -520,16 +557,18 @@ namespace Barotrauma } else { + float colliderLength = GetColliderLength(); door = currentWaypoint.ConnectedDoor; if (door.LinkedGap.IsHorizontal) { int dir = Math.Sign(nextWaypoint.WorldPosition.X - door.Item.WorldPosition.X); - shouldBeOpen = (door.Item.WorldPosition.X - character.WorldPosition.X) * dir > -50.0f; + float size = character.AnimController.InWater ? colliderLength : GetColliderSize().X; + shouldBeOpen = (door.Item.WorldPosition.X - character.WorldPosition.X) * dir > -size; } else { int dir = Math.Sign(nextWaypoint.WorldPosition.Y - door.Item.WorldPosition.Y); - shouldBeOpen = (door.Item.WorldPosition.Y - character.WorldPosition.Y) * dir > -80.0f; + shouldBeOpen = (door.Item.WorldPosition.Y - character.WorldPosition.Y) * dir > -colliderLength; } } } @@ -573,7 +612,7 @@ namespace Barotrauma } else if (closestButton != null) { - if (Vector2.DistanceSquared(closestButton.Item.WorldPosition, character.WorldPosition) < MathUtils.Pow(closestButton.Item.InteractDistance * 2, 2)) + if (Vector2.DistanceSquared(closestButton.Item.WorldPosition, character.WorldPosition) < MathUtils.Pow(closestButton.Item.InteractDistance + GetColliderLength(), 2)) { closestButton.Item.TryInteract(character, false, true); buttonPressCooldown = ButtonPressInterval; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs index 9ffdcf361..90c63c8b8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs @@ -19,16 +19,19 @@ namespace Barotrauma private Body targetBody; private Vector2 attachSurfaceNormal; private Submarine targetSubmarine; + private Character targetCharacter; private readonly Character character; public bool AttachToSub { get; private set; } public bool AttachToWalls { get; private set; } + public bool AttachToCharacters { get; private set; } - private readonly float minDeattachSpeed, maxDeattachSpeed; + private readonly float minDeattachSpeed, maxDeattachSpeed, maxAttachDuration; private readonly float damageOnDetach, detachStun; - private float deattachTimer; + private readonly bool weld; + private float deattachCheckTimer; - private Vector2 wallAttachPos; + private Vector2 _attachPos; private float attachCooldown; @@ -38,9 +41,9 @@ namespace Barotrauma private float jointDir; - public List AttachJoints { get; } = new List(); + public List AttachJoints { get; } = new List(); - public Vector2? WallAttachPos + public Vector2? AttachPos { get; private set; @@ -48,18 +51,21 @@ namespace Barotrauma public bool IsAttached => AttachJoints.Count > 0; - public bool IsAttachedToSub => IsAttached && targetSubmarine != null; + public bool IsAttachedToSub => IsAttached && targetSubmarine != null && targetCharacter == null; public LatchOntoAI(XElement element, EnemyAIController enemyAI) { AttachToWalls = element.GetAttributeBool("attachtowalls", false); AttachToSub = element.GetAttributeBool("attachtosub", false); + AttachToCharacters = element.GetAttributeBool("attachtocharacters", false); minDeattachSpeed = element.GetAttributeFloat("mindeattachspeed", 5.0f); maxDeattachSpeed = Math.Max(minDeattachSpeed, element.GetAttributeFloat("maxdeattachspeed", 8.0f)); + maxAttachDuration = element.GetAttributeFloat("maxattachduration", -1.0f); damageOnDetach = element.GetAttributeFloat("damageondetach", 0.0f); detachStun = element.GetAttributeFloat("detachstun", 0.0f); localAttachPos = ConvertUnits.ToSimUnits(element.GetAttributeVector2("localattachpos", Vector2.Zero)); attachLimbRotation = MathHelper.ToRadians(element.GetAttributeFloat("attachlimbrotation", 0.0f)); + weld = element.GetAttributeBool("weld", true); string limbString = element.GetAttributeString("attachlimb", null); attachLimb = enemyAI.Character.AnimController.Limbs.FirstOrDefault(l => string.Equals(l.Name, limbString, StringComparison.OrdinalIgnoreCase)); @@ -81,30 +87,54 @@ namespace Barotrauma public void SetAttachTarget(Structure wall, Vector2 attachPos, Vector2 attachSurfaceNormal) { + if (!AttachToSub) { return; } if (wall == null) { return; } var sub = wall.Submarine; if (sub == null) { return; } + Reset(); targetWall = wall; targetSubmarine = sub; targetBody = targetSubmarine.PhysicsBody.FarseerBody; this.attachSurfaceNormal = attachSurfaceNormal; - wallAttachPos = attachPos; + _attachPos = attachPos; + } + + public void SetAttachTarget(Character target) + { + if (!AttachToCharacters) { return; } + Reset(); + targetCharacter = target; + targetSubmarine = target.Submarine; + targetBody = target.AnimController.Collider.FarseerBody; + attachSurfaceNormal = Vector2.Normalize(character.WorldPosition - target.WorldPosition); } public void Update(EnemyAIController enemyAI, float deltaTime) { if (character.Submarine != null) { - DeattachFromBody(reset: true); - return; + if (targetCharacter != null && targetCharacter.Submarine != targetSubmarine || + character.Submarine != null && targetSubmarine != null && targetCharacter == null) + { + DeattachFromBody(reset: true); + return; + } } - if (AttachJoints.Count > 0) + if (IsAttached) { 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; + var attachJoint = AttachJoints[0]; + if (attachJoint is WeldJoint weldJoint) + { + weldJoint.LocalAnchorA = new Vector2(-weldJoint.LocalAnchorA.X, weldJoint.LocalAnchorA.Y); + weldJoint.ReferenceAngle = -weldJoint.ReferenceAngle; + } + else if (attachJoint is RevoluteJoint revoluteJoint) + { + revoluteJoint.LocalAnchorA = new Vector2(-revoluteJoint.LocalAnchorA.X, revoluteJoint.LocalAnchorA.Y); + revoluteJoint.ReferenceAngle = -revoluteJoint.ReferenceAngle; + } jointDir = attachLimb.Dir; } for (int i = 0; i < AttachJoints.Count; i++) @@ -113,31 +143,51 @@ namespace Barotrauma 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"); + DebugConsole.Log("Limb body of the character \"" + character.Name + "\" is very far from the attach joint anchor -> deattach"); #endif DeattachFromBody(reset: true); return; } } + if (targetCharacter != null) + { + if (enemyAI.AttackingLimb?.attack == null) + { + DeattachFromBody(reset: true, cooldown: 1); + } + else + { + float range = enemyAI.AttackingLimb.attack.DamageRange * 2f; + if (Vector2.DistanceSquared(targetCharacter.WorldPosition, enemyAI.AttackingLimb.WorldPosition) > range * range) + { + DeattachFromBody(reset: true, cooldown: 1); + } + } + } } if (attachCooldown > 0) { attachCooldown -= deltaTime; } - if (deattachTimer > 0) + if (deattachCheckTimer > 0) { - deattachTimer -= deltaTime; + deattachCheckTimer -= deltaTime; } - Vector2 transformedAttachPos = wallAttachPos; + if (targetCharacter != null) + { + // Own sim pos -> target where we are + _attachPos = character.SimPosition; + } + Vector2 transformedAttachPos = _attachPos; if (character.Submarine == null && targetSubmarine != null) { transformedAttachPos += ConvertUnits.ToSimUnits(targetSubmarine.Position); } if (transformedAttachPos != Vector2.Zero) { - WallAttachPos = transformedAttachPos; + AttachPos = transformedAttachPos; } switch (enemyAI.State) @@ -151,7 +201,7 @@ namespace Barotrauma //check if there are any walls nearby the character could attach to if (raycastTimer < 0.0f) { - wallAttachPos = Vector2.Zero; + _attachPos = Vector2.Zero; var cells = Level.Loaded.GetCells(character.WorldPosition, 1); if (cells.Count > 0) @@ -169,7 +219,7 @@ namespace Barotrauma { attachSurfaceNormal = edge.GetNormal(cell); targetBody = cell.Body; - wallAttachPos = potentialAttachPos; + _attachPos = potentialAttachPos; closestDist = distSqr; } break; @@ -183,21 +233,20 @@ namespace Barotrauma } else { - wallAttachPos = Vector2.Zero; + _attachPos = Vector2.Zero; } - - if (wallAttachPos == Vector2.Zero || targetBody == null) + if (_attachPos == Vector2.Zero || targetBody == null) { DeattachFromBody(reset: false); } else { - float squaredDistance = Vector2.DistanceSquared(character.SimPosition, wallAttachPos); + float squaredDistance = Vector2.DistanceSquared(character.SimPosition, _attachPos); float targetDistance = Math.Max(Math.Max(character.AnimController.Collider.radius, character.AnimController.Collider.width), character.AnimController.Collider.height) * 1.2f; if (squaredDistance < targetDistance * targetDistance) { //close enough to a wall -> attach - AttachToBody(wallAttachPos); + AttachToBody(_attachPos); enemyAI.SteeringManager.Reset(); } else @@ -205,25 +254,22 @@ namespace Barotrauma //move closer to the wall DeattachFromBody(reset: false); enemyAI.SteeringManager.SteeringAvoid(deltaTime, 1.0f, 0.1f); - enemyAI.SteeringManager.SteeringSeek(wallAttachPos); + enemyAI.SteeringManager.SteeringSeek(_attachPos); } } break; case AIState.Attack: case AIState.Aggressive: - if (enemyAI.AttackingLimb != null) + if (enemyAI.IsSteeringThroughGap) { break; } + if (_attachPos == Vector2.Zero) { break; } + if (!AttachToSub && !AttachToCharacters) { break; } + if (enemyAI.AttackingLimb == null) { break; } + if (targetBody == null) { break; } + if (IsAttached && AttachJoints[0].BodyB == targetBody) { break; } + Vector2 referencePos = targetCharacter != null ? targetCharacter.WorldPosition : ConvertUnits.ToDisplayUnits(transformedAttachPos); + if (Vector2.DistanceSquared(referencePos, enemyAI.AttackingLimb.WorldPosition) < enemyAI.AttackingLimb.attack.DamageRange * enemyAI.AttackingLimb.attack.DamageRange) { - if (AttachToSub && !enemyAI.IsSteeringThroughGap && wallAttachPos != Vector2.Zero && targetBody != null) - { - // is not attached or is attached to something else - 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(transformedAttachPos); - } - } - } + AttachToBody(transformedAttachPos); } break; default: @@ -231,43 +277,50 @@ namespace Barotrauma break; } - if (IsAttached && targetBody != null && targetWall != null && targetSubmarine != null && deattachTimer <= 0.0f) + if (IsAttached && targetBody != null && deattachCheckTimer <= 0.0f) { 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)) + if (maxAttachDuration > 0) { deattach = true; - attachCooldown = 2; } - if (!deattach) + if (!deattach && targetWall != null && targetSubmarine != null) { - // Deattach if the velocity is high - float velocity = targetSubmarine.Velocity == Vector2.Zero ? 0.0f : targetSubmarine.Velocity.Length(); - deattach = velocity > maxDeattachSpeed; + // 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)) + { + deattach = true; + attachCooldown = 2; + } if (!deattach) { - if (velocity > minDeattachSpeed) + // Deattach if the velocity is high + float velocity = targetSubmarine.Velocity == Vector2.Zero ? 0.0f : targetSubmarine.Velocity.Length(); + deattach = velocity > maxDeattachSpeed; + if (!deattach) { - 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) + if (velocity > minDeattachSpeed) { - deattach = true; - character.AddDamage(character.WorldPosition, new List() { AfflictionPrefab.InternalDamage.Instantiate(damageOnDetach) }, detachStun, true); - attachCooldown = detachStun * 2; + 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; + } } } } + deattachCheckTimer = 5.0f; } if (deattach) { DeattachFromBody(reset: true); } - deattachTimer = 5.0f; } } @@ -315,16 +368,30 @@ namespace Barotrauma } collider.SetTransform(attachPos + attachSurfaceNormal * colliderFront.Length(), MathUtils.VectorToAngle(-attachSurfaceNormal) - MathHelper.PiOver2); - var colliderJoint = new WeldJoint(collider.FarseerBody, targetBody, colliderFront, targetBody.GetLocalPoint(attachPos), false) - { - FrequencyHz = 10.0f, - DampingRatio = 0.5f, - KinematicBodyB = true, - CollideConnected = false, - //Length = 0.1f - }; + Joint colliderJoint = weld ? + new WeldJoint(collider.FarseerBody, targetBody, colliderFront, targetBody.GetLocalPoint(attachPos), false) + { + FrequencyHz = 10.0f, + DampingRatio = 0.5f, + KinematicBodyB = true, + CollideConnected = false, + } : + new RevoluteJoint(collider.FarseerBody, targetBody, colliderFront, targetBody.GetLocalPoint(attachPos), false) + { + MotorEnabled = true, + MaxMotorTorque = 0.25f + } as Joint; + GameMain.World.Add(colliderJoint); - AttachJoints.Add(colliderJoint); + AttachJoints.Add(colliderJoint); + if (targetCharacter != null) + { + targetCharacter.Latchers.Add(this); + } + if (maxAttachDuration > 0) + { + deattachCheckTimer = maxAttachDuration; + } } public void DeattachFromBody(bool reset, float cooldown = 0) @@ -342,14 +409,23 @@ namespace Barotrauma { Reset(); } + if (targetCharacter != null) + { + targetCharacter.Latchers.Remove(this); + } } private void Reset() { + if (targetCharacter != null) + { + targetCharacter.Latchers.Remove(this); + } + targetCharacter = null; targetWall = null; targetSubmarine = null; targetBody = null; - WallAttachPos = null; + AttachPos = null; } private void OnCharacterDeath(Character character, CauseOfDeath causeOfDeath) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs index 503ef0efb..23bfc48ad 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs @@ -96,6 +96,7 @@ namespace Barotrauma #if DEBUG if (HumanAIController.debugai && objectiveManager.IsOrder(this) && !objectiveManager.IsCurrentOrder() && !objectiveManager.IsCurrentOrder()) { + // TODO: dismiss throw new Exception("Order abandoned!"); } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index 187479017..0557f1eb3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -111,7 +111,7 @@ namespace Barotrauma public CombatMode Mode { get; private set; } private bool IsOffensiveOrArrest => initialMode == CombatMode.Offensive || initialMode == CombatMode.Arrest; - private bool TargetEliminated => IsEnemyDisabled || Enemy.IsUnconscious; + private bool TargetEliminated => IsEnemyDisabled || (Enemy.IsUnconscious && Enemy.Params.Health.ConstantHealthRegeneration <= 0.0f); private bool IsEnemyDisabled => Enemy == null || Enemy.Removed || Enemy.IsDead; private float AimSpeed => HumanAIController.AimSpeed; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs index a28df6cf9..6e13c261d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs @@ -56,7 +56,8 @@ namespace Barotrauma public static bool IsValidTarget(Character target, Character character) { if (target == null || target.Removed) { return false; } - if (target.IsDead || target.IsUnconscious) { return false; } + if (target.IsDead) { return false; } + if (target.IsUnconscious && target.Params.Health.ConstantHealthRegeneration <= 0.0f) { return false; } if (target == character) { return false; } if (target.Submarine == null) { return false; } if (character.Submarine == null) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs index 94dba0146..dc172e587 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs @@ -307,6 +307,8 @@ namespace Barotrauma foreach (Hull hull in Hull.hullList.OrderByDescending(h => EstimateHullSuitability(h))) { if (hull.Submarine == null) { continue; } + // Ruins are mazes filled with water. There's no safe hulls and we don't want to use the resources on it. + if (hull.Submarine.Info.IsRuin) { continue; } if (!allowChangingTheSubmarine && hull.Submarine != character.Submarine) { continue; } if (hull.Rect.Height < ConvertUnits.ToDisplayUnits(character.AnimController.ColliderHeightFromFloor) * 2) { continue; } if (ignoredHulls != null && ignoredHulls.Contains(hull)) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs index 7c4a2e520..ded911eea 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -461,11 +461,11 @@ namespace Barotrauma nodeFilter = n => n.Waypoint.Tunnel != null; } - PathSteering.SteeringSeek(targetPos, 1, - startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (character.CurrentHull == null), - endNodeFilter, - nodeFilter, - CheckVisibility); + PathSteering.SteeringSeek(targetPos, weight: 1, + startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (character.CurrentHull == null), + endNodeFilter: endNodeFilter, + nodeFilter: nodeFilter, + checkVisiblity: CheckVisibility); if (!isInside && (PathSteering.CurrentPath == null || PathSteering.IsPathDirty || PathSteering.CurrentPath.Unreachable)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs index c6cf25146..f91b1d36a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs @@ -167,7 +167,7 @@ namespace Barotrauma private readonly List sortedNodes = new List(); - public SteeringPath FindPath(Vector2 start, Vector2 end, Submarine hostSub = null, string errorMsgStr = null, Func startNodeFilter = null, Func endNodeFilter = null, Func nodeFilter = null, bool checkVisibility = true) + public SteeringPath FindPath(Vector2 start, Vector2 end, Submarine hostSub = null, string errorMsgStr = null, float minGapSize = 0, Func startNodeFilter = null, Func endNodeFilter = null, Func nodeFilter = null, bool checkVisibility = true) { foreach (PathNode node in nodes) { @@ -233,6 +233,10 @@ namespace Barotrauma // Always check the visibility for the start node if (!IsWaypointVisible(node, start)) { continue; } if (node.IsBlocked()) { continue; } + if (node.Waypoint.ConnectedGap != null) + { + if (!CanFitThroughGap(node.Waypoint.ConnectedGap, minGapSize)) { continue; } + } startNode = node; } } @@ -282,6 +286,10 @@ namespace Barotrauma // Only check the visibility for the end node when allowed (fix leaks) if (!IsWaypointVisible(node, end, checkVisibility: checkVisibility)) { continue; } if (node.IsBlocked()) { continue; } + if (node.Waypoint.ConnectedGap != null) + { + if (!CanFitThroughGap(node.Waypoint.ConnectedGap, minGapSize)) { continue; } + } endNode = node; } } @@ -294,40 +302,12 @@ namespace Barotrauma return new SteeringPath(true); } - var path = FindPath(startNode, endNode, nodeFilter, errorMsgStr); + var path = FindPath(startNode, endNode, nodeFilter, errorMsgStr, minGapSize); return path; } - public SteeringPath FindPath(WayPoint start, WayPoint end) - { - PathNode startNode = null, endNode = null; - foreach (PathNode node in nodes) - { - if (node.Waypoint == start) - { - startNode = node; - if (endNode != null) { break; } - } - if (node.Waypoint == end) - { - endNode = node; - if (startNode != null) { break; } - } - } - - if (startNode == null || endNode == null) - { -#if DEBUG - DebugConsole.NewMessage("Pathfinding error, couldn't find matching pathnodes to waypoints.", Color.DarkRed); -#endif - return new SteeringPath(true); - } - - return FindPath(startNode, endNode); - } - - private SteeringPath FindPath(PathNode start, PathNode end, Func filter = null, string errorMsgStr = "") + private SteeringPath FindPath(PathNode start, PathNode end, Func filter = null, string errorMsgStr = "", float minGapSize = 0) { if (start == end) { @@ -356,7 +336,10 @@ namespace Barotrauma if (isCharacter && node.Waypoint.isObstructed) { continue; } if (filter != null && !filter(node)) { continue; } if (node.IsBlocked()) { continue; } - + if (node.Waypoint.ConnectedGap != null) + { + if (!CanFitThroughGap(node.Waypoint.ConnectedGap, minGapSize)) { continue; } + } dist = node.F; currNode = node; } @@ -460,6 +443,8 @@ namespace Barotrauma return path; } + + private bool CanFitThroughGap(Gap gap, float minWidth) => gap.IsHorizontal ? gap.RectHeight > minWidth : gap.RectWidth > minWidth; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs index f03fabde1..4bad55106 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs @@ -1,12 +1,31 @@ -using FarseerPhysics; +using Barotrauma.Items.Components; +using FarseerPhysics; using Microsoft.Xna.Framework; -using System.Collections.Generic; using System; +using System.Collections.Generic; +using System.Linq; +using Barotrauma.Extensions; namespace Barotrauma { abstract class AnimController : Ragdoll { + public Vector2 RightHandIKPos { get; protected set; } + public Vector2 LeftHandIKPos { get; protected set; } + + protected LimbJoint rightShoulder, leftShoulder; + protected float upperArmLength, forearmLength; + protected float useItemTimer; + protected bool aiming; + protected bool wasAiming; + protected bool aimingMelee; + protected bool wasAimingMelee; + + public bool IsAiming => wasAiming; + public bool IsAimingMelee => wasAimingMelee; + + public float ArmLength => upperArmLength + forearmLength; + public abstract GroundedMovementParams WalkParams { get; set; } public abstract GroundedMovementParams RunParams { get; set; } public abstract SwimParams SwimSlowParams { get; set; } @@ -60,14 +79,14 @@ namespace Barotrauma } else { - return IsMovingFast? SwimFastParams : SwimSlowParams; + return IsMovingFast ? SwimFastParams : SwimSlowParams; } } } public bool CanWalk => RagdollParams.CanWalk; public bool IsMovingBackwards => !InWater && Math.Sign(targetMovement.X) == -Math.Sign(Dir); - + // TODO: define death anim duration in XML protected float deathAnimTimer, deathAnimDuration = 5.0f; @@ -155,13 +174,9 @@ namespace Barotrauma public AnimController(Character character, string seed, RagdollParams ragdollParams = null) : base(character, seed, ragdollParams) { } - public virtual void UpdateAnim(float deltaTime) { } + public abstract void UpdateAnim(float deltaTime); - public virtual void HoldItem(float deltaTime, Item item, Vector2[] handlePos, Vector2 holdPos, Vector2 aimPos, bool aim, float holdAngle, float itemAngleRelativeToHoldAngle = 0.0f, bool aimingMelee = false) { } - - public virtual void DragCharacter(Character target, float deltaTime) { } - - public virtual void UpdateUseItem(bool allowMovement, Vector2 handWorldPos) { } + public abstract void DragCharacter(Character target, float deltaTime); public virtual float GetSpeed(AnimationType type) { @@ -253,5 +268,437 @@ namespace Barotrauma throw new NotImplementedException(type.ToString()); } } + + public void UpdateUseItem(bool allowMovement, Vector2 handWorldPos) + { + useItemTimer = 0.5f; + Anim = Animation.UsingConstruction; + + if (!allowMovement) + { + TargetMovement = Vector2.Zero; + TargetDir = handWorldPos.X > character.WorldPosition.X ? Direction.Right : Direction.Left; + float sqrDist = Vector2.DistanceSquared(character.WorldPosition, handWorldPos); + if (sqrDist > MathUtils.Pow(ConvertUnits.ToDisplayUnits(upperArmLength + forearmLength), 2)) + { + TargetMovement = Vector2.Normalize(handWorldPos - character.WorldPosition) * GetCurrentSpeed(false) * Math.Max(character.SpeedMultiplier, 1); + } + } + + if (!character.Enabled) { return; } + + Vector2 handSimPos = ConvertUnits.ToSimUnits(handWorldPos); + if (character.Submarine != null) + { + handSimPos -= character.Submarine.SimPosition; + } + + var leftHand = GetLimb(LimbType.LeftHand); + if (leftHand != null) + { + leftHand.Disabled = true; + leftHand.PullJointEnabled = true; + leftHand.PullJointWorldAnchorB = handSimPos; + } + + var rightHand = GetLimb(LimbType.RightHand); + if (rightHand != null) + { + rightHand.Disabled = true; + rightHand.PullJointEnabled = true; + rightHand.PullJointWorldAnchorB = handSimPos; + } + } + + public void Grab(Vector2 rightHandPos, Vector2 leftHandPos) + { + for (int i = 0; i < 2; i++) + { + Limb pullLimb = (i == 0) ? GetLimb(LimbType.LeftHand) : GetLimb(LimbType.RightHand); + + pullLimb.Disabled = true; + + pullLimb.PullJointEnabled = true; + pullLimb.PullJointWorldAnchorB = (i == 0) ? rightHandPos : leftHandPos; + pullLimb.PullJointMaxForce = 500.0f; + } + } + + private Direction previousDirection; + private readonly Vector2[] transformedHandlePos = new Vector2[2]; + //TODO: refactor this method, it's way too convoluted + public void HoldItem(float deltaTime, Item item, Vector2[] handlePos, Vector2 holdPos, Vector2 aimPos, bool aim, float holdAngle, float itemAngleRelativeToHoldAngle = 0.0f, bool aimMelee = false) + { + aimingMelee = aimMelee; + if (character.Stun > 0.0f || character.IsIncapacitated) + { + aim = false; + } + + //calculate the handle positions + Matrix itemTransfrom = Matrix.CreateRotationZ(item.body.Rotation); + float horizontalOffset = ConvertUnits.ToSimUnits((item.Sprite.size.X / 2 - item.Sprite.Origin.X) * item.Scale); + + //handlePos[0] = ConvertUnits.ToSimUnits(new Vector2(-45,25) * 0.5f); + //handlePos[1] = ConvertUnits.ToSimUnits(new Vector2(-65,30) * 0.5f); + + transformedHandlePos[0] = Vector2.Transform(new Vector2(handlePos[0].X + horizontalOffset, handlePos[0].Y), itemTransfrom); + transformedHandlePos[1] = Vector2.Transform(new Vector2(handlePos[1].X + horizontalOffset, handlePos[1].Y), itemTransfrom); + + Limb torso = GetLimb(LimbType.Torso) ?? MainLimb; + Limb leftHand = GetLimb(LimbType.LeftHand); + Limb rightHand = GetLimb(LimbType.RightHand); + + Vector2 itemPos = aim ? aimPos : holdPos; + + var controller = character.SelectedConstruction?.GetComponent(); + bool usingController = controller != null && !controller.AllowAiming; + bool isClimbing = character.IsClimbing && Math.Abs(character.AnimController.TargetMovement.Y) > 0.01f; + float itemAngle; + Holdable holdable = item.GetComponent(); + float torsoRotation = torso.Rotation; + bool equippedInRightHand = character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand) == item && rightHand != null && !rightHand.IsSevered; + bool equippedInLefthand = character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand) == item && leftHand != null && !leftHand.IsSevered; + if (aim && !isClimbing && !usingController && character.Stun <= 0.0f && itemPos != Vector2.Zero && !character.IsIncapacitated) + { + Vector2 mousePos = ConvertUnits.ToSimUnits(character.SmoothedCursorPosition); + Vector2 diff = holdable.Aimable ? (mousePos - AimSourceSimPos) * Dir : Vector2.UnitX; + holdAngle = MathUtils.VectorToAngle(new Vector2(diff.X, diff.Y * Dir)) - torsoRotation * Dir; + holdAngle += GetAimWobble(rightHand, leftHand, item); + itemAngle = torsoRotation + holdAngle * Dir; + if (holdable.ControlPose) + { + var head = GetLimb(LimbType.Head); + if (head != null) + { + head.body.SmoothRotate(itemAngle, force: 30 * head.Mass); + } + if (TargetMovement == Vector2.Zero && inWater) + { + torso.body.AngularVelocity -= torso.body.AngularVelocity * 0.1f; + torso.body.ApplyForce(torso.body.LinearVelocity * -0.5f); + } + aiming = true; + } + } + else + { + if (holdable.UseHandRotationForHoldAngle) + { + if (equippedInRightHand) + { + itemAngle = rightHand.Rotation + holdAngle * Dir; + } + else if (equippedInLefthand) + { + itemAngle = leftHand.Rotation + holdAngle * Dir; + } + else + { + itemAngle = torsoRotation + holdAngle * Dir; + } + } + else + { + itemAngle = torsoRotation + holdAngle * Dir; + } + } + + if (rightShoulder == null) { return; } + Vector2 transformedHoldPos = rightShoulder.WorldAnchorA; + if (itemPos == Vector2.Zero || isClimbing || usingController) + { + if (equippedInRightHand) + { + transformedHoldPos = rightHand.PullJointWorldAnchorA - transformedHandlePos[0]; + itemAngle = rightHand.Rotation + (holdAngle - rightHand.Params.GetSpriteOrientation() + MathHelper.PiOver2) * Dir; + } + else if (equippedInLefthand) + { + transformedHoldPos = leftHand.PullJointWorldAnchorA - transformedHandlePos[1]; + itemAngle = leftHand.Rotation + (holdAngle - leftHand.Params.GetSpriteOrientation() + MathHelper.PiOver2) * Dir; + } + } + else + { + if (equippedInRightHand) + { + transformedHoldPos = rightShoulder.WorldAnchorA; + rightHand.Disabled = true; + } + if (equippedInLefthand) + { + if (leftShoulder == null) { return; } + transformedHoldPos = leftShoulder.WorldAnchorA; + leftHand.Disabled = true; + } + itemPos.X *= Dir; + transformedHoldPos += Vector2.Transform(itemPos, Matrix.CreateRotationZ(itemAngle)); + } + + item.body.ResetDynamics(); + + Vector2 currItemPos = equippedInRightHand ? + rightHand.PullJointWorldAnchorA - transformedHandlePos[0] : + leftHand.PullJointWorldAnchorA - transformedHandlePos[1]; + + if (!MathUtils.IsValid(currItemPos)) + { + string errorMsg = "Attempted to move the item \"" + item + "\" to an invalid position in HumanidAnimController.HoldItem: " + + currItemPos + ", rightHandPos: " + rightHand.PullJointWorldAnchorA + ", leftHandPos: " + leftHand.PullJointWorldAnchorA + + ", handlePos[0]: " + handlePos[0] + ", handlePos[1]: " + handlePos[1] + + ", transformedHandlePos[0]: " + transformedHandlePos[0] + ", transformedHandlePos[1]:" + transformedHandlePos[1] + + ", item pos: " + item.SimPosition + ", itemAngle: " + itemAngle + + ", collider pos: " + character.SimPosition; + DebugConsole.Log(errorMsg); + GameAnalyticsManager.AddErrorEventOnce( + "HumanoidAnimController.HoldItem:InvalidPos:" + character.Name + item.Name, + GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + errorMsg); + + return; + } + + if (holdable.Pusher != null) + { + if (character.Stun > 0.0f || character.IsIncapacitated) + { + holdable.Pusher.Enabled = false; + } + else + { + if (!holdable.Pusher.Enabled) + { + holdable.Pusher.Enabled = true; + holdable.Pusher.ResetDynamics(); + holdable.Pusher.SetTransform(currItemPos, itemAngle); + } + else + { + holdable.Pusher.TargetPosition = currItemPos; + holdable.Pusher.TargetRotation = holdAngle * Dir; + + holdable.Pusher.MoveToTargetPosition(true); + + currItemPos = holdable.Pusher.SimPosition; + itemAngle = holdable.Pusher.Rotation; + } + } + } + float targetAngle = MathUtils.WrapAngleTwoPi(itemAngle + itemAngleRelativeToHoldAngle * Dir); + float currentRotation = MathUtils.WrapAngleTwoPi(item.body.Rotation); + float itemRotation = MathHelper.SmoothStep(currentRotation, targetAngle, deltaTime * 25); + if (previousDirection != dir || Math.Abs(targetAngle - currentRotation) > MathHelper.Pi) + { + itemRotation = targetAngle; + } + item.SetTransform(currItemPos, itemRotation, setPrevTransform: false); + previousDirection = dir; + + if (!isClimbing && !character.IsIncapacitated && itemPos != Vector2.Zero && (aim || !holdable.UseHandRotationForHoldAngle)) + { + for (int i = 0; i < 2; i++) + { + if (!character.Inventory.IsInLimbSlot(item, i == 0 ? InvSlotType.RightHand : InvSlotType.LeftHand)) { continue; } +#if DEBUG + if (handlePos[i].LengthSquared() > ArmLength) + { + DebugConsole.AddWarning($"Aim position for the item {item.Name} may be incorrect (further than the length of the character's arm)"); + } +#endif + HandIK(i == 0 ? rightHand : leftHand, transformedHoldPos + transformedHandlePos[i], CurrentAnimationParams.ArmIKStrength, CurrentAnimationParams.HandIKStrength); + } + } + } + + private float GetAimWobble(Limb rightHand, Limb leftHand, Item heldItem) + { + float wobbleStrength = 0.0f; + if (character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand) == heldItem) + { + wobbleStrength += Character.CharacterHealth.GetLimbDamage(rightHand, afflictionType: "damage"); + } + if (character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand) == heldItem) + { + wobbleStrength += Character.CharacterHealth.GetLimbDamage(leftHand, afflictionType: "damage"); + } + if (wobbleStrength <= 0.1f) { return 0.0f; } + wobbleStrength = (float)Math.Min(wobbleStrength, 1.0f); + + float lowFreqNoise = PerlinNoise.GetPerlin((float)Timing.TotalTime / 320.0f, (float)Timing.TotalTime / 240.0f) - 0.5f; + float highFreqNoise = PerlinNoise.GetPerlin((float)Timing.TotalTime / 40.0f, (float)Timing.TotalTime / 50.0f) - 0.5f; + + return (lowFreqNoise * 1.0f + highFreqNoise * 0.1f) * wobbleStrength; + } + + public void HandIK(Limb hand, Vector2 pos, float armTorque = 1.0f, float handTorque = 1.0f) + { + Vector2 shoulderPos; + + Limb arm, forearm; + if (hand.type == LimbType.LeftHand) + { + if (leftShoulder == null) { return; } + shoulderPos = leftShoulder.WorldAnchorA; + arm = GetLimb(LimbType.LeftArm); + forearm = GetLimb(LimbType.LeftForearm); + LeftHandIKPos = pos; + } + else + { + if (rightShoulder == null) { return; } + shoulderPos = rightShoulder.WorldAnchorA; + arm = GetLimb(LimbType.RightArm); + forearm = GetLimb(LimbType.RightForearm); + RightHandIKPos = pos; + } + if (arm == null) { return; } + + //distance from shoulder to holdpos + float c = Vector2.Distance(pos, shoulderPos); + c = MathHelper.Clamp(c, Math.Abs(upperArmLength - forearmLength), forearmLength + upperArmLength - 0.01f); + + float armAngle = MathUtils.VectorToAngle(pos - shoulderPos) + arm.Params.GetSpriteOrientation() - MathHelper.PiOver2; + float upperArmAngle = MathUtils.SolveTriangleSSS(forearmLength, upperArmLength, c) * Dir; + float lowerArmAngle = MathUtils.SolveTriangleSSS(upperArmLength, forearmLength, c) * Dir; + + //make sure the arm angle "has the same number of revolutions" as the arm + while (arm.Rotation - armAngle > MathHelper.Pi) + { + armAngle += MathHelper.TwoPi; + } + while (arm.Rotation - armAngle < -MathHelper.Pi) + { + armAngle -= MathHelper.TwoPi; + } + + arm?.body.SmoothRotate(armAngle - upperArmAngle, 100.0f * armTorque * arm.Mass, wrapAngle: false); + float forearmAngle = armAngle + lowerArmAngle; + forearm?.body.SmoothRotate(forearmAngle, 100.0f * handTorque * forearm.Mass, wrapAngle: false); + float handAngle = forearm != null ? forearmAngle : armAngle; + hand?.body.SmoothRotate(handAngle, 10.0f * handTorque * hand.Mass, wrapAngle: false); + } + + public void ApplyPose(Vector2 leftHandPos, Vector2 rightHandPos, Vector2 leftFootPos, Vector2 rightFootPos, float footMoveForce = 10) + { + var leftHand = GetLimb(LimbType.LeftHand); + var rightHand = GetLimb(LimbType.RightHand); + var waist = GetLimb(LimbType.Waist) ?? GetLimb(LimbType.Torso); + if (waist == null) { return; } + Vector2 midPos = waist.SimPosition; + if (leftHand != null) + { + leftHand.Disabled = true; + leftHandPos.X *= Dir; + leftHandPos += midPos; + HandIK(leftHand, leftHandPos); + } + if (rightHand != null) + { + rightHand.Disabled = true; + rightHandPos.X *= Dir; + rightHandPos += midPos; + HandIK(rightHand, rightHandPos); + } + var leftFoot = GetLimb(LimbType.LeftFoot); + if (leftFoot != null) + { + leftFoot.Disabled = true; + leftFootPos = new Vector2(waist.SimPosition.X + leftFootPos.X * Dir, GetColliderBottom().Y + leftFootPos.Y); + MoveLimb(leftFoot, leftFootPos, Math.Abs(leftFoot.SimPosition.X - leftFootPos.X) * footMoveForce * leftFoot.Mass, true); + } + var rightFoot = GetLimb(LimbType.RightFoot); + if (rightFoot != null) + { + rightFoot.Disabled = true; + rightFootPos = new Vector2(waist.SimPosition.X + rightFootPos.X * Dir, GetColliderBottom().Y + rightFootPos.Y); + MoveLimb(rightFoot, rightFootPos, Math.Abs(rightFoot.SimPosition.X - rightFootPos.X) * footMoveForce * rightFoot.Mass, true); + } + } + + public void ApplyTestPose() + { + var waist = GetLimb(LimbType.Waist) ?? GetLimb(LimbType.Torso); + if (waist != null) + { + ApplyPose( + new Vector2(-0.75f, -0.2f), + new Vector2(0.75f, -0.2f), + new Vector2(-WalkParams.StepSize.X * 0.5f, -0.1f * RagdollParams.JointScale), + new Vector2(WalkParams.StepSize.X * 0.5f, -0.1f * RagdollParams.JointScale)); + } + } + + protected void CalculateArmLengths() + { + //calculate arm and forearm length (atm this assumes that both arms are the same size) + Limb rightForearm = GetLimb(LimbType.RightForearm); + Limb rightHand = GetLimb(LimbType.RightHand); + if (rightHand == null) { return; } + + rightShoulder = GetJointBetweenLimbs(LimbType.Torso, LimbType.RightArm) ?? GetJointBetweenLimbs(LimbType.Head, LimbType.RightArm) ?? GetJoint(LimbType.RightArm, new LimbType[] { LimbType.RightHand, LimbType.RightForearm }); + leftShoulder = GetJointBetweenLimbs(LimbType.Torso, LimbType.LeftArm) ?? GetJointBetweenLimbs(LimbType.Head, LimbType.LeftArm) ?? GetJoint(LimbType.LeftArm, new LimbType[] { LimbType.LeftHand, LimbType.LeftForearm }); + + Vector2 localAnchorShoulder = Vector2.Zero; + Vector2 localAnchorElbow = Vector2.Zero; + if (rightShoulder != null) + { + localAnchorShoulder = rightShoulder.LimbA.type == LimbType.RightArm ? rightShoulder.LocalAnchorA : rightShoulder.LocalAnchorB; + } + LimbJoint rightElbow = rightForearm == null ? + GetJointBetweenLimbs(LimbType.RightArm, LimbType.RightHand) : + GetJointBetweenLimbs(LimbType.RightArm, LimbType.RightForearm); + if (rightElbow != null) + { + localAnchorElbow = rightElbow.LimbA.type == LimbType.RightArm ? rightElbow.LocalAnchorA : rightElbow.LocalAnchorB; + } + upperArmLength = Vector2.Distance(localAnchorShoulder, localAnchorElbow); + if (rightElbow != null) + { + if (rightForearm == null) + { + forearmLength = Vector2.Distance( + rightHand.PullJointLocalAnchorA, + rightElbow.LimbA.type == LimbType.RightHand ? rightElbow.LocalAnchorA : rightElbow.LocalAnchorB); + } + else + { + LimbJoint rightWrist = GetJointBetweenLimbs(LimbType.RightForearm, LimbType.RightHand); + if (rightWrist != null) + { + forearmLength = Vector2.Distance( + rightElbow.LimbA.type == LimbType.RightForearm ? rightElbow.LocalAnchorA : rightElbow.LocalAnchorB, + rightWrist.LimbA.type == LimbType.RightForearm ? rightWrist.LocalAnchorA : rightWrist.LocalAnchorB); + + forearmLength += Vector2.Distance( + rightHand.PullJointLocalAnchorA, + rightElbow.LimbA.type == LimbType.RightHand ? rightElbow.LocalAnchorA : rightElbow.LocalAnchorB); + } + } + } + } + + protected LimbJoint GetJointBetweenLimbs(LimbType limbTypeA, LimbType limbTypeB) + { + return LimbJoints.FirstOrDefault(lj => + (lj.LimbA.type == limbTypeA && lj.LimbB.type == limbTypeB) || + (lj.LimbB.type == limbTypeA && lj.LimbA.type == limbTypeB)); + } + + protected LimbJoint GetJoint(LimbType matchingType, IEnumerable ignoredTypes) + { + return LimbJoints.FirstOrDefault(lj => + lj.LimbA.type == matchingType && ignoredTypes.None(t => lj.LimbB.type == t) || + lj.LimbB.type == matchingType && ignoredTypes.None(t => lj.LimbB.type == t)); + } + + public override void Recreate(RagdollParams ragdollParams = null) + { + base.Recreate(ragdollParams); + if (Character.Params.CanInteract) + { + CalculateArmLengths(); + } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs index 8edee5073..c22e642f0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs @@ -139,7 +139,7 @@ namespace Barotrauma if (MainLimb == null) { return; } var mainLimb = MainLimb; - levitatingCollider = true; + levitatingCollider = !IsHanging; if (!character.CanMove) { @@ -192,6 +192,11 @@ namespace Barotrauma strongestImpact = 0.0f; } + if (aiming) + { + TargetMovement = TargetMovement.ClampLength(2); + } + if (inWater && !forceStanding) { Collider.FarseerBody.FixedRotation = false; @@ -202,7 +207,7 @@ namespace Barotrauma if (CurrentGroundedParams != null) { //rotate collider back upright - float standAngle = dir == Direction.Right ? CurrentGroundedParams.ColliderStandAngleInRadians : -CurrentGroundedParams.ColliderStandAngleInRadians; + float standAngle = CurrentGroundedParams.ColliderStandAngleInRadians * Dir; if (Math.Abs(MathUtils.GetShortestAngle(Collider.Rotation, standAngle)) > 0.001f) { Collider.AngularVelocity = MathUtils.GetShortestAngle(Collider.Rotation, standAngle) * 60.0f; @@ -215,17 +220,19 @@ namespace Barotrauma } UpdateWalkAnim(deltaTime); } - if (character.SelectedCharacter != null) { DragCharacter(character.SelectedCharacter, deltaTime); return; } - + if (character.AnimController.AnimationTestPose) + { + ApplyTestPose(); + } //don't flip when simply physics is enabled if (SimplePhysicsEnabled) { return; } - if (!character.IsRemotelyControlled && (character.AIController == null || character.AIController.CanFlip)) + if (!character.IsRemotelyControlled && (character.AIController == null || character.AIController.CanFlip) && !aiming) { if (!inWater || (CurrentSwimParams != null && CurrentSwimParams.Mirror)) { @@ -290,6 +297,10 @@ namespace Barotrauma { flipTimer = 0.0f; } + wasAiming = aiming; + aiming = false; + wasAimingMelee = aimingMelee; + aimingMelee = false; } private bool CanDrag(Character target) @@ -449,6 +460,16 @@ namespace Barotrauma //limbs are disabled when simple physics is enabled, no need to move them if (SimplePhysicsEnabled) { return; } mainLimb.PullJointEnabled = true; + + if (aiming && movement.Length() <= 0.1f) + { + Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition); + Vector2 diff = (mousePos - (GetLimb(LimbType.Torso) ?? MainLimb).SimPosition) * Dir; + TargetMovement = new Vector2(0.0f, -0.1f); + float newRotation = MathUtils.VectorToAngle(diff); + Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier); + } + if (!isMoving) { WalkPos = MathHelper.SmoothStep(WalkPos, MathHelper.PiOver2, deltaTime * 5); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index 722cc1106..0eb6e0eb3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -146,34 +146,8 @@ namespace Barotrauma public bool Crouching; - private float upperArmLength = 0.0f, forearmLength = 0.0f; - - public float ArmLength => upperArmLength + forearmLength; - - public Vector2 RightHandIKPos - { - get; - private set; - } - public Vector2 LeftHandIKPos - { - get; - private set; - } - - private LimbJoint rightShoulder, leftShoulder; - private float upperLegLength = 0.0f, lowerLegLength = 0.0f; - private bool aiming; - private bool wasAiming; - - private bool aimingMelee; - private bool wasAimingMelee; - - public bool IsAiming => wasAiming; - public bool IsAimingMelee => wasAimingMelee; - private readonly float movementLerp; private float cprAnimTimer; @@ -184,7 +158,6 @@ namespace Barotrauma //prevents rapid switches between swimming/walking if the water level is fluctuating around the minimum swimming depth private float swimmingStateLockTimer; - private float useItemTimer; public float HeadLeanAmount => CurrentGroundedParams.HeadLeanAmount; public float TorsoLeanAmount => CurrentGroundedParams.TorsoLeanAmount; public Vector2 FootMoveOffset => CurrentGroundedParams.FootMoveOffset * RagdollParams.JointScale; @@ -219,61 +192,12 @@ namespace Barotrauma movementLerp = RagdollParams.MainElement.GetAttributeFloat("movementlerp", 0.4f); } - public override void Recreate(RagdollParams ragdollParams) + public override void Recreate(RagdollParams ragdollParams = null) { base.Recreate(ragdollParams); - CalculateArmLengths(); CalculateLegLengths(); } - private void CalculateArmLengths() - { - //calculate arm and forearm length (atm this assumes that both arms are the same size) - Limb rightForearm = GetLimb(LimbType.RightForearm); - Limb rightHand = GetLimb(LimbType.RightHand); - if (rightHand == null) { return; } - - rightShoulder = GetJointBetweenLimbs(LimbType.Torso, LimbType.RightArm); - leftShoulder = GetJointBetweenLimbs(LimbType.Torso, LimbType.LeftArm); - Vector2 localAnchorShoulder = Vector2.Zero; - Vector2 localAnchorElbow = Vector2.Zero; - if (rightShoulder != null) - { - localAnchorShoulder = rightShoulder.LimbA.type == LimbType.RightArm ? rightShoulder.LocalAnchorA : rightShoulder.LocalAnchorB; - } - LimbJoint rightElbow = rightForearm == null ? - GetJointBetweenLimbs(LimbType.RightArm, LimbType.RightHand) : - GetJointBetweenLimbs(LimbType.RightArm, LimbType.RightForearm); - if (rightElbow != null) - { - localAnchorElbow = rightElbow.LimbA.type == LimbType.RightArm ? rightElbow.LocalAnchorA : rightElbow.LocalAnchorB; - } - upperArmLength = Vector2.Distance(localAnchorShoulder, localAnchorElbow); - if (rightElbow != null) - { - if (rightForearm == null) - { - forearmLength = Vector2.Distance( - rightHand.PullJointLocalAnchorA, - rightElbow.LimbA.type == LimbType.RightHand ? rightElbow.LocalAnchorA : rightElbow.LocalAnchorB); - } - else - { - LimbJoint rightWrist = GetJointBetweenLimbs(LimbType.RightForearm, LimbType.RightHand); - if (rightWrist != null) - { - forearmLength = Vector2.Distance( - rightElbow.LimbA.type == LimbType.RightForearm ? rightElbow.LocalAnchorA : rightElbow.LocalAnchorB, - rightWrist.LimbA.type == LimbType.RightForearm ? rightWrist.LocalAnchorA : rightWrist.LocalAnchorB); - - forearmLength += Vector2.Distance( - rightHand.PullJointLocalAnchorA, - rightElbow.LimbA.type == LimbType.RightHand ? rightElbow.LocalAnchorA : rightElbow.LocalAnchorB); - } - } - } - } - private void CalculateLegLengths() { //calculate upper and lower leg length (atm this assumes that both legs are the same size) @@ -304,21 +228,16 @@ namespace Barotrauma ankleJoint.LimbA.type == footType ? ankleJoint.LocalAnchorA : ankleJoint.LocalAnchorB, GetLimb(footType).PullJointLocalAnchorA); } - private LimbJoint GetJointBetweenLimbs(LimbType limbTypeA, LimbType limbTypeB) - { - return LimbJoints.FirstOrDefault(lj => - (lj.LimbA.type == limbTypeA && lj.LimbB.type == limbTypeB) || - (lj.LimbB.type == limbTypeA && lj.LimbA.type == limbTypeB)); - } public override void UpdateAnim(float deltaTime) { if (Frozen) return; if (MainLimb == null) { return; } - levitatingCollider = true; + levitatingCollider = !IsHanging; ColliderIndex = Crouching && !swimming ? 1 : 0; if (character.SelectedConstruction?.GetComponent()?.ControlCharacterPose ?? false || + character.SelectedConstruction?.GetComponent() != null || (ForceSelectAnimationType != AnimationType.Crouch && ForceSelectAnimationType != AnimationType.NotDefined)) { Crouching = false; @@ -422,41 +341,25 @@ namespace Barotrauma midPos += Vector2.Transform(new Vector2(-0.3f * Dir, -0.2f), torsoTransform); if (rightHand.PullJointEnabled) midPos = (midPos + rightHand.PullJointWorldAnchorB) / 2.0f; - HandIK(rightHand, midPos, CurrentHumanAnimParams.ArmIKStrength, CurrentHumanAnimParams.HandIKStrength); - HandIK(leftHand, midPos, CurrentHumanAnimParams.ArmIKStrength, CurrentHumanAnimParams.HandIKStrength); + HandIK(rightHand, midPos, CurrentAnimationParams.ArmIKStrength, CurrentAnimationParams.HandIKStrength); + HandIK(leftHand, midPos, CurrentAnimationParams.ArmIKStrength, CurrentAnimationParams.HandIKStrength); } else if (character.AnimController.AnimationTestPose) { - var leftHand = GetLimb(LimbType.LeftHand); - var rightHand = GetLimb(LimbType.RightHand); - var waist = GetLimb(LimbType.Waist) ?? GetLimb(LimbType.Torso); - rightHand.Disabled = true; - leftHand.Disabled = true; - Vector2 midPos = waist.SimPosition; - HandIK(rightHand, midPos + new Vector2(-1, -0.2f) * Dir); - HandIK(leftHand, midPos + new Vector2(1, -0.2f) * Dir); - - var leftFoot = GetLimb(LimbType.LeftFoot); - var rightFoot = GetLimb(LimbType.RightFoot); - rightFoot.Disabled = true; - leftFoot.Disabled = true; - // The code here is a bit obscure, but it's pretty much copy-pasted from the block that is used for crouching. - for (int i = -1; i < 2; i += 2) - { - Vector2 footPos = GetColliderBottom(); - footPos = new Vector2(waist.SimPosition.X + Math.Sign(WalkParams.StepSize.X * i) * Dir * 0.3f, footPos.Y - 0.1f * RagdollParams.JointScale); - var foot = i == -1 ? rightFoot : leftFoot; - MoveLimb(foot, footPos, Math.Abs(foot.SimPosition.X - footPos.X) * 100.0f, true); - } + ApplyTestPose(); } else { - if (Anim != Animation.UsingConstruction) ResetPullJoints(); + if (Anim != Animation.UsingConstruction) + { + ResetPullJoints(); + } } if (SimplePhysicsEnabled) { UpdateStandingSimple(); + IsHanging = false; return; } @@ -520,12 +423,11 @@ namespace Barotrauma { limb.Disabled = false; } - wasAiming = aiming; aiming = false; wasAimingMelee = aimingMelee; aimingMelee = false; - if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) return; + IsHanging = false; } void UpdateStanding() @@ -844,16 +746,18 @@ namespace Barotrauma var arm = GetLimb(armType); if (arm != null && Math.Abs(arm.body.AngularVelocity) < 10.0f) { - arm.body.SmoothRotate(MathHelper.Clamp(-arm.body.AngularVelocity, -0.1f, 0.1f), arm.Mass * 10.0f); + arm.body.SmoothRotate(MathHelper.Clamp(-arm.body.AngularVelocity, -0.5f, 0.5f), arm.Mass * 50.0f); } //get the elbow to a neutral rotation if (Math.Abs(hand.body.AngularVelocity) < 10.0f) { - LimbJoint elbow = GetJointBetweenLimbs(armType, hand.type) ?? GetJointBetweenLimbs(armType, foreArmType); + var forearm = GetLimb(foreArmType) ?? hand; + LimbJoint elbow = GetJointBetweenLimbs(armType, foreArmType) ?? GetJointBetweenLimbs(armType, hand.type); if (elbow != null) { - hand.body.ApplyTorque(MathHelper.Clamp(-elbow.JointAngle, -MathHelper.PiOver2, MathHelper.PiOver2) * hand.Mass * 10.0f); + float diff = elbow.JointAngle - (Dir > 0 ? elbow.LowerLimit : elbow.UpperLimit); + forearm.body.ApplyTorque(MathHelper.Clamp(-diff, -MathHelper.PiOver2, MathHelper.PiOver2) * forearm.Mass * 100.0f); } } } @@ -1099,6 +1003,7 @@ namespace Barotrauma rightHandPos.X = (Dir == 1.0f) ? Math.Max(0.3f, rightHandPos.X) : Math.Min(-0.3f, rightHandPos.X); rightHandPos = Vector2.Transform(rightHandPos, rotationMatrix); float speedMultiplier = character.SpeedMultiplier * (1 - Character.GetRightHandPenalty()); + // Limb hand, Vector2 pos, float force = 1.0f HandIK(rightHand, handPos + rightHandPos, CurrentSwimParams.ArmMoveStrength * speedMultiplier, CurrentSwimParams.HandMoveStrength * speedMultiplier); } @@ -1390,6 +1295,8 @@ namespace Barotrauma { target.Oxygen += deltaTime * 0.5f; //Stabilize them } + + bool powerfulCPR = character.HasAbilityFlag(AbilityFlags.PowerfulCPR); int skill = (int)character.GetSkillLevel("medical"); //pump for 15 seconds (cprAnimTimer 0-15), then do mouth-to-mouth for 2 seconds (cprAnimTimer 15-17) @@ -1406,13 +1313,19 @@ namespace Barotrauma { if (target.Oxygen < -10.0f) { - //stabilize the oxygen level but don't allow it to go positive and revive the character yet - float stabilizationAmount = skill * CPRSettings.StabilizationPerSkill; - stabilizationAmount = MathHelper.Clamp(stabilizationAmount, CPRSettings.StabilizationMin, CPRSettings.StabilizationMax); - character.Oxygen -= (1.0f / stabilizationAmount) * deltaTime; //Worse skill = more oxygen required - if (character.Oxygen > 0.0f) target.Oxygen += stabilizationAmount * deltaTime; //we didn't suffocate yet did we - - //DebugConsole.NewMessage("CPR Us: " + character.Oxygen + " Them: " + target.Oxygen + " How good we are: restore " + cpr + " use " + (30.0f - cpr), Color.Aqua); + if (powerfulCPR) + { + //prevent the patient from suffocating no matter how fast their oxygen level is dropping + target.Oxygen = Math.Max(target.Oxygen, -10.0f); + } + else + { + //stabilize the oxygen level but don't allow it to go positive and revive the character yet + float stabilizationAmount = skill * CPRSettings.StabilizationPerSkill; + stabilizationAmount = MathHelper.Clamp(stabilizationAmount, CPRSettings.StabilizationMin, CPRSettings.StabilizationMax); + character.Oxygen -= 1.0f / stabilizationAmount * deltaTime; //Worse skill = more oxygen required + if (character.Oxygen > 0.0f) { target.Oxygen += stabilizationAmount * deltaTime; } //we didn't suffocate yet did we + } } } } @@ -1447,6 +1360,8 @@ namespace Barotrauma reviveChance = (float)Math.Pow(reviveChance, CPRSettings.ReviveChanceExponent); reviveChance = MathHelper.Clamp(reviveChance, CPRSettings.ReviveChanceMin, CPRSettings.ReviveChanceMax); + if (powerfulCPR) { reviveChance *= 2.0f; } + if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) <= reviveChance) { //increase oxygen and clamp it above zero @@ -1706,248 +1621,6 @@ namespace Barotrauma } } - public void Grab(Vector2 rightHandPos, Vector2 leftHandPos) - { - for (int i = 0; i < 2; i++) - { - Limb pullLimb = (i == 0) ? GetLimb(LimbType.LeftHand) : GetLimb(LimbType.RightHand); - - pullLimb.Disabled = true; - - pullLimb.PullJointEnabled = true; - pullLimb.PullJointWorldAnchorB = (i == 0) ? rightHandPos : leftHandPos; - pullLimb.PullJointMaxForce = 500.0f; - } - } - - //TODO: refactor this method, it's way too convoluted - public override void HoldItem(float deltaTime, Item item, Vector2[] handlePos, Vector2 holdPos, Vector2 aimPos, bool aim, float holdAngle, float itemAngleRelativeToHoldAngle = 0.0f, bool aimingMelee = false) - { - if (character.Stun > 0.0f || character.IsIncapacitated) - { - aim = false; - } - - //calculate the handle positions - Matrix itemTransfrom = Matrix.CreateRotationZ(item.body.Rotation); - // TODO: don't create new arrays, reuse - Vector2[] transformedHandlePos = new Vector2[2]; - transformedHandlePos[0] = Vector2.Transform(handlePos[0], itemTransfrom); - transformedHandlePos[1] = Vector2.Transform(handlePos[1], itemTransfrom); - - Limb head = GetLimb(LimbType.Head); - Limb torso = GetLimb(LimbType.Torso); - Limb leftHand = GetLimb(LimbType.LeftHand); - Limb rightHand = GetLimb(LimbType.RightHand); - - // TODO: Remove this. Provide the position in params. - Vector2 itemPos = aim ? aimPos : holdPos; - - var controller = character.SelectedConstruction?.GetComponent(); - bool usingController = controller != null && !controller.AllowAiming; - bool isClimbing = character.IsClimbing && Math.Abs(character.AnimController.TargetMovement.Y) > 0.01f; - - float itemAngle; - - Holdable holdable = item.GetComponent(); - - this.aimingMelee = aimingMelee; - - if (!isClimbing && !usingController && character.Stun <= 0.0f && aim && itemPos != Vector2.Zero && !character.IsIncapacitated) - { - Vector2 mousePos = ConvertUnits.ToSimUnits(character.SmoothedCursorPosition); - - Vector2 diff = holdable.Aimable ? (mousePos - AimSourceSimPos) * Dir : Vector2.UnitX; - - holdAngle = MathUtils.VectorToAngle(new Vector2(diff.X, diff.Y * Dir)) - torso.body.Rotation * Dir; - holdAngle += GetAimWobble(rightHand, leftHand, item); - - itemAngle = torso.body.Rotation + holdAngle * Dir; - - if (holdable.ControlPose) - { - head?.body.SmoothRotate(itemAngle); - - if (TargetMovement == Vector2.Zero && inWater) - { - torso.body.AngularVelocity -= torso.body.AngularVelocity * 0.1f; - torso.body.ApplyForce(torso.body.LinearVelocity * -0.5f); - } - - aiming = true; - } - } - else - { - itemAngle = torso.body.Rotation + holdAngle * Dir; - } - - Vector2 transformedHoldPos = rightShoulder.WorldAnchorA; - if (itemPos == Vector2.Zero || isClimbing || usingController) - { - if (character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand) == item) - { - if (rightHand == null || rightHand.IsSevered) { return; } - transformedHoldPos = rightHand.PullJointWorldAnchorA - transformedHandlePos[0]; - itemAngle = (rightHand.Rotation + (holdAngle - MathHelper.PiOver2) * Dir); - } - else if (character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand) == item) - { - if (leftHand == null || leftHand.IsSevered) { return; } - transformedHoldPos = leftHand.PullJointWorldAnchorA - transformedHandlePos[1]; - itemAngle = (leftHand.Rotation + (holdAngle - MathHelper.PiOver2) * Dir); - } - } - else - { - if (character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand) == item) - { - if (rightHand == null || rightHand.IsSevered) { return; } - transformedHoldPos = rightShoulder.WorldAnchorA; - rightHand.Disabled = true; - } - if (character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand) == item) - { - if (leftHand == null || leftHand.IsSevered) { return; } - transformedHoldPos = leftShoulder.WorldAnchorA; - leftHand.Disabled = true; - } - - itemPos.X *= Dir; - transformedHoldPos += Vector2.Transform(itemPos, Matrix.CreateRotationZ(itemAngle)); - } - - item.body.ResetDynamics(); - - Vector2 currItemPos = (character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand) == item) ? - rightHand.PullJointWorldAnchorA - transformedHandlePos[0] : - leftHand.PullJointWorldAnchorA - transformedHandlePos[1]; - - if (!MathUtils.IsValid(currItemPos)) - { - string errorMsg = "Attempted to move the item \"" + item + "\" to an invalid position in HumanidAnimController.HoldItem: " + - currItemPos + ", rightHandPos: " + rightHand.PullJointWorldAnchorA + ", leftHandPos: " + leftHand.PullJointWorldAnchorA + - ", handlePos[0]: " + handlePos[0] + ", handlePos[1]: " + handlePos[1] + - ", transformedHandlePos[0]: " + transformedHandlePos[0] + ", transformedHandlePos[1]:" + transformedHandlePos[1] + - ", item pos: " + item.SimPosition + ", itemAngle: " + itemAngle + - ", collider pos: " + character.SimPosition; - DebugConsole.Log(errorMsg); - GameAnalyticsManager.AddErrorEventOnce( - "HumanoidAnimController.HoldItem:InvalidPos:" + character.Name + item.Name, - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - errorMsg); - - return; - } - - if (holdable.Pusher != null) - { - if (character.Stun > 0.0f || character.IsIncapacitated) - { - holdable.Pusher.Enabled = false; - } - else - { - if (!holdable.Pusher.Enabled) - { - holdable.Pusher.Enabled = true; - holdable.Pusher.ResetDynamics(); - holdable.Pusher.SetTransform(currItemPos, itemAngle); - } - else - { - holdable.Pusher.TargetPosition = currItemPos; - holdable.Pusher.TargetRotation = holdAngle * Dir; - - holdable.Pusher.MoveToTargetPosition(true); - - currItemPos = holdable.Pusher.SimPosition; - itemAngle = holdable.Pusher.Rotation; - } - } - } - - item.SetTransform(currItemPos, itemAngle + itemAngleRelativeToHoldAngle * Dir, setPrevTransform: false); - - if (!isClimbing && !character.IsIncapacitated && itemPos != Vector2.Zero) - { - for (int i = 0; i < 2; i++) - { - if (!character.Inventory.IsInLimbSlot(item, i == 0 ? InvSlotType.RightHand : InvSlotType.LeftHand)) { continue; } - HandIK(i == 0 ? rightHand : leftHand, transformedHoldPos + transformedHandlePos[i], CurrentHumanAnimParams.ArmIKStrength, CurrentHumanAnimParams.HandIKStrength); - } - } - } - - private float GetAimWobble(Limb rightHand, Limb leftHand, Item heldItem) - { - float wobbleStrength = 0.0f; - if (character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand) == heldItem) - { - wobbleStrength += Character.CharacterHealth.GetLimbDamage(rightHand, afflictionType: "damage"); - } - if (character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand) == heldItem) - { - wobbleStrength += Character.CharacterHealth.GetLimbDamage(leftHand, afflictionType: "damage"); - } - if (wobbleStrength <= 0.1f) { return 0.0f; } - wobbleStrength = (float)Math.Min(wobbleStrength, 1.0f); - - float lowFreqNoise = PerlinNoise.GetPerlin((float)Timing.TotalTime / 320.0f, (float)Timing.TotalTime / 240.0f) - 0.5f; - float highFreqNoise = PerlinNoise.GetPerlin((float)Timing.TotalTime / 40.0f, (float)Timing.TotalTime / 50.0f) - 0.5f; - - return (lowFreqNoise * 1.0f + highFreqNoise * 0.1f) * wobbleStrength; - } - - private void HandIK(Limb hand, Vector2 pos, float armTorque = 1.0f, float handTorque = 1.0f) - { - Vector2 shoulderPos; - - Limb arm, forearm; - if (hand.type == LimbType.LeftHand) - { - if (leftShoulder == null) { return; } - shoulderPos = leftShoulder.WorldAnchorA; - arm = GetLimb(LimbType.LeftArm); - forearm = GetLimb(LimbType.LeftForearm); - LeftHandIKPos = pos; - } - else - { - if (rightShoulder == null) { return; } - shoulderPos = rightShoulder.WorldAnchorA; - arm = GetLimb(LimbType.RightArm); - forearm = GetLimb(LimbType.RightForearm); - RightHandIKPos = pos; - } - if (arm == null) { return; } - - //distance from shoulder to holdpos - float c = Vector2.Distance(pos, shoulderPos); - c = MathHelper.Clamp(c, Math.Abs(upperArmLength - forearmLength), forearmLength + upperArmLength - 0.01f); - - float armAngle = MathUtils.VectorToAngle(pos - shoulderPos) + MathHelper.PiOver2; - - float upperArmAngle = MathUtils.SolveTriangleSSS(forearmLength, upperArmLength, c) * Dir; - float lowerArmAngle = MathUtils.SolveTriangleSSS(upperArmLength, forearmLength, c) * Dir; - - //make sure the arm angle "has the same number of revolutions" as the arm - while (arm.Rotation - armAngle > MathHelper.Pi) - { - armAngle += MathHelper.TwoPi; - } - while (arm.Rotation - armAngle < -MathHelper.Pi) - { - armAngle -= MathHelper.TwoPi; - } - - arm?.body.SmoothRotate(armAngle - upperArmAngle, 100.0f * armTorque * arm.Mass, wrapAngle: false); - float forearmAngle = armAngle + lowerArmAngle; - forearm?.body.SmoothRotate(forearmAngle, 100.0f * handTorque * forearm.Mass, wrapAngle: false); - float handAngle = forearm != null ? armAngle : forearmAngle; - hand?.body.SmoothRotate(handAngle, 100.0f * handTorque * hand.Mass, wrapAngle: false); - } - private void FootIK(Limb foot, Vector2 pos, float legTorque, float footTorque, float footAngle) { if (!MathUtils.IsValid(pos)) @@ -2014,47 +1687,6 @@ namespace Barotrauma foot.body.SmoothRotate((legAngle - (lowerLegAngle + footAngle) * Dir), foot.Mass * footTorque, wrapAngle: false); } - public override void UpdateUseItem(bool allowMovement, Vector2 handWorldPos) - { - useItemTimer = 0.5f; - Anim = Animation.UsingConstruction; - - if (!allowMovement) - { - TargetMovement = Vector2.Zero; - TargetDir = handWorldPos.X > character.WorldPosition.X ? Direction.Right : Direction.Left; - float sqrDist = Vector2.DistanceSquared(character.WorldPosition, handWorldPos); - if (sqrDist > MathUtils.Pow(ConvertUnits.ToDisplayUnits(upperArmLength + forearmLength), 2)) - { - TargetMovement = Vector2.Normalize(handWorldPos - character.WorldPosition) * GetCurrentSpeed(false) * Math.Max(character.SpeedMultiplier, 1); - } - } - - if (!character.Enabled) { return; } - - Vector2 handSimPos = ConvertUnits.ToSimUnits(handWorldPos); - if (character.Submarine != null) - { - handSimPos -= character.Submarine.SimPosition; - } - - var leftHand = GetLimb(LimbType.LeftHand); - if (leftHand != null) - { - leftHand.Disabled = true; - leftHand.PullJointEnabled = true; - leftHand.PullJointWorldAnchorB = handSimPos; - } - - var rightHand = GetLimb(LimbType.RightHand); - if (rightHand != null) - { - rightHand.Disabled = true; - rightHand.PullJointEnabled = true; - rightHand.PullJointWorldAnchorB = handSimPos; - } - } - public override void Flip() { base.Flip(); @@ -2073,7 +1705,8 @@ namespace Barotrauma { heldItem.FlipX(relativeToSub: false); } - heldItem.FlipX(relativeToSub: false); + // TODO: was this added by a mistake? + //heldItem.FlipX(relativeToSub: false); } foreach (Limb limb in Limbs) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index a42e5d81e..03f6be102 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -125,7 +125,8 @@ namespace Barotrauma protected float surfaceY; protected bool inWater, headInWater; - public bool onGround; + protected bool onGround; + public bool OnGround => onGround; private Vector2 lastFloorCheckPos; private bool lastFloorCheckIgnoreStairs, lastFloorCheckIgnorePlatforms; @@ -887,7 +888,7 @@ namespace Barotrauma /// if false, force is applied to the position of pullJoint - protected void MoveLimb(Limb limb, Vector2 pos, float amount, bool pullFromCenter = false) + public void MoveLimb(Limb limb, Vector2 pos, float amount, bool pullFromCenter = false) { limb.MoveToPos(pos, amount, pullFromCenter); } @@ -974,8 +975,7 @@ namespace Barotrauma Vector2 newSubPos = newHull.Submarine == null ? Vector2.Zero : newHull.Submarine.Position; Vector2 prevSubPos = currentHull.Submarine == null ? Vector2.Zero : currentHull.Submarine.Position; - Teleport(ConvertUnits.ToSimUnits(prevSubPos - newSubPos), - Vector2.Zero); + Teleport(ConvertUnits.ToSimUnits(prevSubPos - newSubPos), Vector2.Zero); } } @@ -1099,6 +1099,7 @@ namespace Barotrauma } public bool forceStanding; + public bool forceNotStanding; public void Update(float deltaTime, Camera cam) { @@ -1270,6 +1271,7 @@ namespace Barotrauma } } UpdateProjSpecific(deltaTime, cam); + forceNotStanding = false; } private void CheckBodyInRest(float deltaTime) @@ -1569,7 +1571,7 @@ namespace Barotrauma return closestFraction; }, rayStart, rayEnd, Physics.CollisionStairs | Physics.CollisionPlatform | Physics.CollisionWall | Physics.CollisionLevel); - if (standOnFloorFixture != null) + if (standOnFloorFixture != null && !IsHanging) { standOnFloorY = rayStart.Y + (rayEnd.Y - rayStart.Y) * standOnFloorFraction; if (rayStart.Y - standOnFloorY < Collider.height * 0.5f + Collider.radius + ColliderHeightFromFloor * 1.2f) @@ -1606,6 +1608,13 @@ namespace Barotrauma } if (MainLimb == null) { return; } + if (Character.AIController is EnemyAIController enemyAI && enemyAI.LatchOntoAI != null && enemyAI.LatchOntoAI.IsAttached) + { + enemyAI.LatchOntoAI.DeattachFromBody(reset: true); + } + Character.Latchers.ForEachMod(l => l.DeattachFromBody(reset: true)); + Character.Latchers.Clear(); + Vector2 limbMoveAmount = forceMainLimbToCollider ? simPosition - MainLimb.SimPosition : simPosition - Collider.SimPosition; if (lerp) { @@ -1629,6 +1638,16 @@ namespace Barotrauma } } + public bool IsHanging { get; protected set; } + + public void Hang() + { + ResetPullJoints(); + onGround = false; + levitatingCollider = false; + IsHanging = true; + } + protected void TrySetLimbPosition(Limb limb, Vector2 original, Vector2 simPosition, bool lerp = false, bool ignorePlatforms = true) { Vector2 movePos = simPosition; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 2d748476e..05ac0b9d6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -129,6 +129,8 @@ namespace Barotrauma } } + public readonly HashSet Latchers = new HashSet(); + protected readonly Dictionary activeTeamChanges = new Dictionary(); protected ActiveTeamChange currentTeamChange; const string OriginalTeamIdentifier = "original"; @@ -264,6 +266,7 @@ namespace Barotrauma public readonly CharacterParams Params; public string SpeciesName => Params.SpeciesName; + public string Group => Params.Group; public bool IsHumanoid => Params.Humanoid; public bool IsHusk => Params.Husk; @@ -473,8 +476,7 @@ namespace Barotrauma return true; } } - - public bool CanInteract => AllowInput && IsHumanoid && !LockHands; + public bool CanInteract => AllowInput && Params.CanInteract && !LockHands; // Eating is not implemented for humanoids. If we implement that at some point, we could remove this restriction. public bool CanEat => !IsHumanoid && Params.CanEat && AllowInput && AnimController.GetLimb(LimbType.Head) != null; @@ -592,6 +594,7 @@ namespace Barotrauma get { if (IsUnconscious) { return true; } + if (IsDead) { return true; } return CharacterHealth.Afflictions.Any(a => a.Prefab.AfflictionType == "paralysis" && a.Strength >= a.Prefab.MaxStrength); } } @@ -626,7 +629,7 @@ namespace Barotrauma public float Stun { - get { return IsRagdolled ? 1.0f : CharacterHealth.Stun; } + get { return IsRagdolled && !AnimController.IsHanging ? 1.0f : CharacterHealth.Stun; } set { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } @@ -1172,6 +1175,7 @@ namespace Barotrauma { LoadHeadAttachments(); } + ApplyStatusEffects(ActionType.OnSpawn, 1.0f); } partial void InitProjSpecific(XElement mainElement); @@ -1669,7 +1673,7 @@ namespace Barotrauma } if (!aiControlled && - AnimController.onGround && + AnimController.OnGround && !AnimController.InWater && AnimController.Anim != AnimController.Animation.UsingConstruction && AnimController.Anim != AnimController.Animation.CPR && @@ -2697,6 +2701,11 @@ namespace Barotrauma ApplyStatusEffects(AnimController.InWater ? ActionType.InWater : ActionType.NotInWater, deltaTime); ApplyStatusEffects(ActionType.OnActive, deltaTime); + if (aiTarget != null) + { + aiTarget.InDetectable = false; + } + UpdateControlled(deltaTime, cam); //Health effects @@ -2720,7 +2729,7 @@ namespace Barotrauma //Do ragdoll shenanigans before Stun because it's still technically a stun, innit? Less network updates for us! bool allowRagdoll = GameMain.NetworkMember?.ServerSettings?.AllowRagdollButton ?? true; - bool tooFastToUnragdoll = AnimController.Collider.LinearVelocity.LengthSquared() > 2.5f * 2.5f; + bool tooFastToUnragdoll = AnimController.Collider.LinearVelocity.LengthSquared() > 8.0f * 8.0f; bool wasRagdolled = false; bool selfRagdolled = false; @@ -2838,7 +2847,7 @@ namespace Barotrauma // If the damage is very low, let's not forget so quickly, or we can't cumulate the damage from repair tools (high frequency, low damage) reduction *= 0.5f; } - enemy.Damage = Math.Max(0.0f, enemy.Damage-reduction); + enemy.Damage = Math.Max(0.0f, enemy.Damage - reduction); } } } @@ -3514,7 +3523,7 @@ namespace Barotrauma if (Removed) { return new AttackResult(); } - if (attacker != null && GameMain.NetworkMember != null && !GameMain.NetworkMember.ServerSettings.AllowFriendlyFire) + if (attacker != null && attacker != this && GameMain.NetworkMember != null && !GameMain.NetworkMember.ServerSettings.AllowFriendlyFire) { if (attacker.TeamID == TeamID) { return new AttackResult(); } } @@ -3676,6 +3685,7 @@ namespace Barotrauma { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && !isNetworkMessage) { return; } if (Screen.Selected != GameMain.GameScreen) { return; } + if (newStun > 0 && Params.Health.StunImmunity) { return; } if ((newStun <= Stun && !allowStunDecrease) || !MathUtils.IsValid(newStun)) { return; } if (Math.Sign(newStun) != Math.Sign(Stun)) { @@ -3876,9 +3886,12 @@ namespace Barotrauma AnimController.movement = Vector2.Zero; AnimController.TargetMovement = Vector2.Zero; - foreach (Item heldItem in HeldItems.ToList()) + if (!LockHands) { - heldItem.Drop(this); + foreach (Item heldItem in HeldItems.ToList()) + { + heldItem.Drop(this); + } } SelectedConstruction = null; @@ -4435,6 +4448,11 @@ namespace Barotrauma /// private readonly Dictionary statValues = new Dictionary(); + /// + /// A dictionary with temporary values, updated when the character equips/unequips wearables. Used to reduce unnecessary inventory checking. + /// + private readonly Dictionary wearableStatValues = new Dictionary(); + public float GetStatValue(StatTypes statType) { if (!IsHuman) { return 0f; } @@ -4453,23 +4471,37 @@ namespace Barotrauma // could be optimized by instead updating the Character.cs statvalues dictionary whenever the CharacterInfo.cs values change statValue += Info.GetSavedStatValue(statType); } - - //replace by updating the character wearable stat values when equipping or unequipping wearables - for (int i = 0; i < Inventory.Capacity; i++) + if (wearableStatValues.TryGetValue(statType, out float wearableValue)) { - if (Inventory.SlotTypes[i] != InvSlotType.Any && Inventory.SlotTypes[i] != InvSlotType.LeftHand && Inventory.SlotTypes[i] != InvSlotType.RightHand - && Inventory.GetItemAt(i)?.GetComponent() is Wearable wearable) - { - if (wearable.WearableStatValues.TryGetValue(statType, out float wearableValue)) - { - statValue += wearableValue; - } - } + statValue += wearableValue; } return statValue; } + public void OnWearablesChanged() + { + wearableStatValues.Clear(); + for (int i = 0; i < Inventory.Capacity; i++) + { + if (Inventory.SlotTypes[i] != InvSlotType.Any && Inventory.SlotTypes[i] != InvSlotType.LeftHand && Inventory.SlotTypes[i] != InvSlotType.RightHand + && Inventory.GetItemAt(i)?.GetComponent() is Wearable wearable) + { + foreach (var statValuePair in wearable.WearableStatValues) + { + if (wearableStatValues.ContainsKey(statValuePair.Key)) + { + wearableStatValues[statValuePair.Key] += statValuePair.Value; + } + else + { + wearableStatValues.Add(statValuePair.Key, statValuePair.Value); + } + } + } + } + } + public void ChangeStat(StatTypes statType, float value) { if (statValues.ContainsKey(statType)) @@ -4516,7 +4548,7 @@ namespace Barotrauma public bool HasAbilityFlag(AbilityFlags abilityFlag) { - return abilityFlags.Contains(abilityFlag); + return abilityFlags.Contains(abilityFlag) || CharacterHealth.HasFlag(abilityFlag); } private readonly Dictionary abilityResistances = new Dictionary(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index 1627fac16..d6a817716 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -218,30 +218,30 @@ namespace Barotrauma public int AdditionalTalentPoints { get; set; } - private Sprite headSprite; + private Sprite _headSprite; public Sprite HeadSprite { get { - if (headSprite == null) + if (_headSprite == null) { LoadHeadSprite(); } #if CLIENT - if (headSprite != null) + if (_headSprite != null) { - CalculateHeadPosition(headSprite); + CalculateHeadPosition(_headSprite); } #endif - return headSprite; + return _headSprite; } private set { - if (headSprite != null) + if (_headSprite != null) { - headSprite.Remove(); + _headSprite.Remove(); } - headSprite = value; + _headSprite = value; } } @@ -287,6 +287,7 @@ namespace Barotrauma Character.CharacterHealth.ApplyAffliction(Character.AnimController.GetLimb(LimbType.Head), AfflictionPrefab.List.FirstOrDefault(a => a.Identifier.Equals("disguised", StringComparison.OrdinalIgnoreCase)).Instantiate(100f)); } + idCard ??= Character.Inventory?.GetItemInLimbSlot(InvSlotType.Card)?.GetComponent(); if (idCard != null) { #if CLIENT @@ -294,19 +295,6 @@ namespace Barotrauma #endif return; } - - if (Character.Inventory != null) - { - idCard = Character.Inventory.GetItemInLimbSlot(InvSlotType.Card)?.GetComponent(); - if (idCard != null) - { -#if CLIENT - GetDisguisedSprites(idCard); -#endif - return; - } - - } } #if CLIENT @@ -1085,7 +1073,7 @@ namespace Barotrauma } } - private static List AddEmpty(IEnumerable elements, WearableType type, float commonness = 1) + public 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)); @@ -1094,9 +1082,9 @@ namespace Barotrauma return list; } - private XElement GetRandomElement(IEnumerable elements) + public XElement GetRandomElement(IEnumerable elements) { - var filtered = elements.Where(e => IsWearableAllowed(e)); + var filtered = elements.Where(IsWearableAllowed); 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; @@ -1121,7 +1109,7 @@ namespace Barotrauma return true; } - private static bool IsValidIndex(int index, List list) => index >= 0 && index < list.Count; + public 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)); @@ -1219,8 +1207,8 @@ namespace Barotrauma OnExperienceChanged(prevAmount, ExperiencePoints, Character.Position + Vector2.UnitY * 150.0f); } - const int BaseExperienceRequired = 150; - const int AddedExperienceRequiredPerLevel = 350; + const int BaseExperienceRequired = 50; + const int AddedExperienceRequiredPerLevel = 450; public int GetTotalTalentPoints() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs index 9cf760566..347b52096 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs @@ -191,6 +191,30 @@ namespace Barotrauma (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); } + public Color GetFaceTint() + { + if (Strength < Prefab.ActivationThreshold) { return Color.TransparentBlack; } + AfflictionPrefab.Effect currentEffect = GetActiveEffect(); + if (currentEffect == null) { return Color.TransparentBlack; } + + return Color.Lerp( + currentEffect.MinFaceTint, + currentEffect.MaxFaceTint, + (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); + } + + public Color GetBodyTint() + { + if (Strength < Prefab.ActivationThreshold) { return Color.TransparentBlack; } + AfflictionPrefab.Effect currentEffect = GetActiveEffect(); + if (currentEffect == null) { return Color.TransparentBlack; } + + return Color.Lerp( + currentEffect.MinBodyTint, + currentEffect.MaxBodyTint, + (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); + } + public float GetScreenBlurStrength() { if (Strength < Prefab.ActivationThreshold) { return 0.0f; } @@ -277,6 +301,13 @@ namespace Barotrauma return 0.0f; } + public bool HasFlag(AbilityFlags flagType) + { + if (!(GetViableEffect() is AfflictionPrefab.Effect currentEffect)) { return false; } + + return currentEffect.AfflictionAbilityFlags.Contains(flagType); + } + private AfflictionPrefab.Effect GetViableEffect() { if (Strength < Prefab.ActivationThreshold) { return null; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs index 7007563e0..0cf50429f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Xml.Linq; using System; using Barotrauma.Extensions; +using Microsoft.Xna.Framework; namespace Barotrauma { @@ -216,6 +217,11 @@ namespace Barotrauma XElement parentElement = new XElement("CharacterInfo"); XElement infoElement = character.Info?.Save(parentElement); CharacterInfo huskCharacterInfo = infoElement == null ? null : new CharacterInfo(infoElement); + + var bodyTint = GetBodyTint(); + huskCharacterInfo.SkinColor = + Color.Lerp(huskCharacterInfo.SkinColor, bodyTint.Opaque(), bodyTint.A / 255.0f); + var husk = Character.Create(huskedSpeciesName, character.WorldPosition, ToolBox.RandomSeed(8), huskCharacterInfo, isRemotePlayer: false, hasAi: true); if (husk.Info != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index f227a11c0..f527262ad 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -1,11 +1,10 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Abilities; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Linq; using System.Reflection; using System.Xml.Linq; -using System.Linq; -using System.Security.Cryptography; -using Barotrauma.Abilities; namespace Barotrauma { @@ -223,7 +222,20 @@ namespace Barotrauma [Serialize("", false)] public string DialogFlag { get; private set; } + [Serialize("0,0,0,0", false)] + public Color MinFaceTint { get; private set; } + + [Serialize("0,0,0,0", false)] + public Color MaxFaceTint { get; private set; } + + [Serialize("0,0,0,0", false)] + public Color MinBodyTint { get; private set; } + + [Serialize("0,0,0,0", false)] + public Color MaxBodyTint { get; private set; } + public readonly Dictionary AfflictionStatValues = new Dictionary(); + public readonly HashSet AfflictionAbilityFlags = new HashSet(); //statuseffects applied on the character when the affliction is active public readonly List StatusEffects = new List(); @@ -250,6 +262,10 @@ namespace Barotrauma AfflictionStatValues.TryAdd(statType, (minValue, maxValue)); break; + case "abilityflag": + var flagType = CharacterAbilityGroup.ParseFlagType(subElement.GetAttributeString("flagtype", ""), parentDebugName); + AfflictionAbilityFlags.Add(flagType); + break; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index 83ff1bad4..91fb404b5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -133,7 +133,7 @@ namespace Barotrauma public bool IsUnconscious { - get { return Vitality <= 0.0f || Character.IsDead; } + get { return (Vitality <= 0.0f || Character.IsDead) && !Character.HasAbilityFlag(AbilityFlags.AlwaysStayConscious); } } public float PressureKillDelay { get; private set; } = 5.0f; @@ -169,6 +169,20 @@ namespace Barotrauma } } + public Color DefaultFaceTint = Color.TransparentBlack; + + public Color FaceTint + { + get; + private set; + } + + public Color BodyTint + { + get; + private set; + } + public float OxygenAmount { get @@ -409,7 +423,7 @@ namespace Barotrauma return strength; } - public void ApplyAffliction(Limb targetLimb, Affliction affliction) + public void ApplyAffliction(Limb targetLimb, Affliction affliction, bool allowStacking = true) { if (!affliction.Prefab.IsBuff && Unkillable || Character.GodMode) { return; } if (affliction.Prefab.LimbSpecific) @@ -419,17 +433,17 @@ namespace Barotrauma //if a limb-specific affliction is applied to no specific limb, apply to all limbs foreach (LimbHealth limbHealth in limbHealths) { - AddLimbAffliction(limbHealth, affliction); + AddLimbAffliction(limbHealth, affliction, allowStacking: allowStacking); } } else { - AddLimbAffliction(targetLimb, affliction); + AddLimbAffliction(targetLimb, affliction, allowStacking: allowStacking); } } else { - AddAffliction(affliction); + AddAffliction(affliction, allowStacking: allowStacking); } } @@ -453,6 +467,15 @@ namespace Barotrauma return value; } + public bool HasFlag(AbilityFlags flagType) + { + for (int i = 0; i < afflictions.Count; i++) + { + if (afflictions[i].HasFlag(flagType)) { return true; } + } + return false; + } + private readonly List matchingAfflictions = new List(); public void ReduceAffliction(Limb targetLimb, string affliction, float amount, ActionType? treatmentAction = null) { @@ -671,6 +694,7 @@ namespace Barotrauma private void AddAffliction(Affliction newAffliction, bool allowStacking = true) { if (!DoesBleed && newAffliction is AfflictionBleeding) { return; } + if (Character.Params.Health.StunImmunity && newAffliction.Prefab.AfflictionType == "stun") { return; } if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) { return; } if (newAffliction.Prefab is AfflictionPrefabHusk huskPrefab) { @@ -725,6 +749,8 @@ namespace Barotrauma StunTimer = Stun > 0 ? StunTimer + deltaTime : 0; + FaceTint = DefaultFaceTint; + for (int i = 0; i < limbHealths.Count; i++) { for (int j = limbHealths[i].Afflictions.Count - 1; j >= 0; j--) @@ -749,10 +775,14 @@ namespace Barotrauma { UpdateBleedingProjSpecific(bleeding, targetLimb, deltaTime); } + Color faceTint = affliction.GetFaceTint(); + if (faceTint.A > FaceTint.A) { FaceTint = faceTint; } + Color bodyTint = affliction.GetBodyTint(); + if (bodyTint.A > BodyTint.A) { BodyTint = bodyTint; } Character.StackSpeedMultiplier(affliction.GetSpeedMultiplier()); } } - + for (int i = afflictions.Count - 1; i >= 0; i--) { var affliction = afflictions[i]; @@ -768,6 +798,10 @@ namespace Barotrauma var affliction = afflictions[i]; affliction.Update(this, null, deltaTime); affliction.DamagePerSecondTimer += deltaTime; + Color faceTint = affliction.GetFaceTint(); + if (faceTint.A > FaceTint.A) { FaceTint = faceTint; } + Color bodyTint = affliction.GetBodyTint(); + if (bodyTint.A > BodyTint.A) { BodyTint = bodyTint; } Character.StackSpeedMultiplier(affliction.GetSpeedMultiplier()); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index fa7d525ae..d0df6e71f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -543,6 +543,8 @@ namespace Barotrauma get { if (character.IsHumanoid) { return false; } + // TODO: We might need this or solve the cases where a limb is severed while holding on to an item + //if (character.Params.CanInteract) { return false; } if (this == character.AnimController.MainLimb) { return false; } if (character.AnimController.CanWalk) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs index eb6290596..26fce7320 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs @@ -129,6 +129,12 @@ namespace Barotrauma [Serialize(AnimationType.NotDefined, true), Editable] public virtual AnimationType AnimationType { get; protected set; } + [Serialize(1f, true, description: "How much force is used to rotate the arms to the IK position."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] + public float ArmIKStrength { get; set; } + + [Serialize(1f, true, description: "How much force is used to rotate the hands to the IK position."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] + public float HandIKStrength { get; set; } + public static string GetDefaultFileName(string speciesName, AnimationType animType) => $"{speciesName.CapitaliseFirstInvariant()}{animType}"; public static string GetDefaultFile(string speciesName, AnimationType animType) => Path.Combine(GetFolder(speciesName), $"{GetDefaultFileName(speciesName, animType)}.xml"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs index 391c1fff8..4192bd331 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs @@ -94,12 +94,6 @@ namespace Barotrauma [Serialize(1f, true, description: "How much force is used to move the hands."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] public float HandMoveStrength { get; set; } - - [Serialize(1f, true, description: "How much force is used to rotate the arms to the IK position."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] - public float ArmIKStrength { get; set; } - - [Serialize(1f, true, description: "How much force is used to rotate the hands to the IK position."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] - public float HandIKStrength { get; set; } } abstract class HumanGroundedParams : GroundedMovementParams, IHumanAnimation @@ -153,12 +147,6 @@ namespace Barotrauma [Serialize(1f, true, description: "How much force is used to move the hands."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] public float HandMoveStrength { get; set; } - - [Serialize(1f, true, description: "How much force is used to rotate the arms to the IK position."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] - public float ArmIKStrength { get; set; } - - [Serialize(1f, true, description: "How much force is used to rotate the hands to the IK position."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] - public float HandIKStrength { get; set; } } public interface IHumanAnimation @@ -169,9 +157,5 @@ namespace Barotrauma float ArmMoveStrength { get; set; } float HandMoveStrength { get; set; } - - float ArmIKStrength { get; set; } - - float HandIKStrength { get; set; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index 59e92989f..e6d40d1bc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -34,6 +34,9 @@ namespace Barotrauma [Serialize(false, true), Editable(ReadOnly = true)] public bool HasInfo { get; private set; } + [Serialize(false, true, description: "Can the creature interact with items?"), Editable] + public bool CanInteract { get; private set; } + [Serialize(false, true), Editable] public bool Husk { get; private set; } @@ -454,6 +457,9 @@ namespace Barotrauma [Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] public float HealthRegenerationWhenEating { get; private set; } + [Serialize(false, true), Editable] + public bool StunImmunity { get; set; } + // TODO: limbhealths, sprite? public HealthParams(XElement element, CharacterParams character) : base(element, character) { } @@ -553,15 +559,24 @@ 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; } - [Serialize(false, true, description:"Does the creature know how to open doors (still requires a proper ID card). Only applies on humanoids. Humans can always open doors (They don't use this AI definition)."), Editable] + [Serialize(false, true, description:"Does the creature know how to open doors (still requires a proper ID card). Humans can always open doors (They don't use this AI definition)."), Editable] public bool CanOpenDoors { get; private set; } + [Serialize(false, true, description: "Does the creature close the doors behind it. Humans don't use this AI definition."), Editable] + public bool KeepDoorsClosed { get; private set; } + [Serialize(true, true, "Is the creature allowed to navigate from and into the depths of the abyss? When enabled, the creatures will try to avoid the depths."), Editable] public bool AvoidAbyss { get; set; } [Serialize(false, true, "Does the creature try to keep in the abyss? Has effect only when AvoidAbyss is false."), Editable] public bool StayInAbyss { get; set; } + [Serialize(false, true, "Does the creature patrol the flooded hulls while idling inside a friendly submarine?"), Editable] + public bool PatrolFlooded { get; set; } + + [Serialize(false, true, "Does the creature patrol the dry hulls while idling inside a friendly submarine?"), Editable] + public bool PatrolDry { get; set; } + [Serialize(0f, true, description: ""), Editable] public float StartAggression { get; private set; } @@ -682,8 +697,14 @@ namespace Barotrauma [Serialize(false, true), Editable] public bool IgnoreIncapacitated { get; set; } - [Serialize(0f, true, description: "How much damage the protected target should take from an attacker before the creature starts defending it."), Editable] - public float DamageThreshold { get; private set; } + [Serialize(0f, true, description: "A generic threshold. For example, how much damage the protected target should take from an attacker before the creature starts defending it."), Editable] + public float Threshold { get; private set; } + + [Serialize(-1f, true, description: "A generic min threshold. Not used if set to negative."), Editable] + public float ThresholdMin { get; private set; } + + [Serialize(-1f, true, description: "A generic max threshold. Not used if set to negative."), Editable] + public float ThresholdMax { get; private set; } [Serialize(AttackPattern.Straight, true), Editable] public AttackPattern AttackPattern { get; set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs index a5be463ca..3b17f0739 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs @@ -37,7 +37,7 @@ namespace Barotrauma [Serialize("1.0,1.0,1.0,1.0", true), Editable()] public Color Color { get; set; } - [Serialize(0.0f, true, description: "The orientation of the sprites as drawn on the sprite sheet. Can be overridden by setting a value for Limb's 'Sprite Orientation'. Used mainly for animations and widgets."), Editable(-360, 360)] + [Serialize(0.0f, true, description: "The orientation of the sprites as drawn on the sprite sheet. Can be overridden by setting a value for Limb's 'Sprite Orientation'."), Editable(-360, 360)] public float SpritesheetOrientation { get; set; } public bool IsSpritesheetOrientationHorizontal @@ -572,7 +572,9 @@ namespace Barotrauma /// /// The orientation of the sprite as drawn on the sprite sheet (in radians). /// - public float GetSpriteOrientation() => MathHelper.ToRadians(float.IsNaN(SpriteOrientation) ? Ragdoll.SpritesheetOrientation : SpriteOrientation); + public float GetSpriteOrientation() => MathHelper.ToRadians(GetSpriteOrientationInDegrees()); + + public float GetSpriteOrientationInDegrees() => float.IsNaN(SpriteOrientation) ? Ragdoll.SpritesheetOrientation : SpriteOrientation; [Serialize("", true), Editable] public string Notes { get; set; } @@ -592,7 +594,7 @@ namespace Barotrauma [Serialize(false, true, description: "Disable drawing for this limb."), Editable()] public bool Hide { get; set; } - [Serialize(float.NaN, true, description: "The orientation of the sprite as drawn on the sprite sheet. Overrides the value defined in the Ragdoll settings. Used mainly for animations and widgets."), Editable(-360, 360, ValueStep = 90, DecimalCount = 0)] + [Serialize(float.NaN, true, description: "The orientation of the sprite as drawn on the sprite sheet. Overrides the value defined in the Ragdoll settings."), Editable(-360, 360, ValueStep = 90, DecimalCount = 0)] public float SpriteOrientation { get; set; } [Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500)] diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackResult.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackResult.cs index a5f321518..58616eac5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackResult.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackResult.cs @@ -23,7 +23,7 @@ namespace Barotrauma.Abilities if (afflictions.Any()) { - if (!afflictions.Any(a => attackResult.Afflictions.Select(c => c.Identifier).Contains(a))) { return false; } + if (attackResult.Afflictions == null || !afflictions.Any(a => attackResult.Afflictions.Select(c => c.Identifier).Contains(a))) { return false; } } return true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionScavenger.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemOutsideSubmarine.cs similarity index 69% rename from Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionScavenger.cs rename to Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemOutsideSubmarine.cs index e1ca65e72..d23794f56 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionScavenger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemOutsideSubmarine.cs @@ -2,10 +2,10 @@ namespace Barotrauma.Abilities { - class AbilityConditionScavenger : AbilityConditionData + class AbilityConditionItemOutsideSubmarine : AbilityConditionData { - public AbilityConditionScavenger(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { } + public AbilityConditionItemOutsideSubmarine(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { } protected override bool MatchesConditionSpecific(AbilityObject abilityObject) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionScrounger.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemWreck.cs similarity index 82% rename from Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionScrounger.cs rename to Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemWreck.cs index 9fc5b00cb..81d1b1d06 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionScrounger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemWreck.cs @@ -2,10 +2,10 @@ namespace Barotrauma.Abilities { - class AbilityConditionScrounger : AbilityConditionData + class AbilityConditionItemWreck : AbilityConditionData { - public AbilityConditionScrounger(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { } + public AbilityConditionItemWreck(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { } protected override bool MatchesConditionSpecific(AbilityObject abilityObject) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs index 0fee25880..2c3b26a5c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs @@ -5,19 +5,25 @@ namespace Barotrauma.Abilities { class AbilityConditionHasPermanentStat : AbilityConditionDataless { + private readonly string statIdentifier; private readonly StatTypes statType; private readonly float min; public AbilityConditionHasPermanentStat(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { - statType = CharacterAbilityGroup.ParseStatType(conditionElement.GetAttributeString("stattype", ""), characterTalent.DebugIdentifier); + statIdentifier = conditionElement.GetAttributeString("statidentifier", string.Empty); + if (string.IsNullOrEmpty(statIdentifier)) + { + DebugConsole.ThrowError($"No stat identifier defined for {this} in talent {characterTalent.DebugIdentifier}!"); + } + string statTypeName = conditionElement.GetAttributeString("stattype", string.Empty); + statType = string.IsNullOrEmpty(statTypeName) ? StatTypes.None : CharacterAbilityGroup.ParseStatType(statTypeName, characterTalent.DebugIdentifier); min = conditionElement.GetAttributeFloat("min", 0f); } protected override bool MatchesConditionSpecific() { - // should consider decoupling this from stat values entirely - return character.Info.GetSavedStatValue(statType) >= min; + return character.Info.GetSavedStatValue(statType, statIdentifier) >= min; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasSkill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasSkill.cs new file mode 100644 index 000000000..60d5da1f7 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasSkill.cs @@ -0,0 +1,24 @@ +using Barotrauma.Items.Components; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionHasSkill : AbilityConditionDataless + { + private readonly string skillIdentifier; + private readonly float minValue; + + public AbilityConditionHasSkill(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + skillIdentifier = conditionElement.GetAttributeString("skillidentifier", string.Empty); + minValue = conditionElement.GetAttributeFloat("minvalue", 0f); + } + + protected override bool MatchesConditionSpecific() + { + return character.GetSkillLevel(skillIdentifier) >= minValue; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityObjects.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityObjects.cs index 6939b18af..7d47baf54 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityObjects.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityObjects.cs @@ -2,7 +2,7 @@ namespace Barotrauma.Abilities { - class AbilityObject + abstract class AbilityObject { // kept as blank for now, as we are using a composition and only using this object to enforce parameter types } @@ -160,6 +160,22 @@ namespace Barotrauma.Abilities } } + class AbilityApplyTreatment : AbilityObject, IAbilityCharacter, IAbilityItem + { + public Character Character { get; set; } + + public Character User { get; set; } + + public Item Item { get; set; } + + public AbilityApplyTreatment(Character user, Character target, Item item) + { + Character = target; + User = user; + Item = item; + } + } + class AbilityAttackResult : AbilityObject, IAbilityAttackResult { public AttackResult AttackResult { get; set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs index ceaa14ee8..ffde9bcdb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs @@ -62,7 +62,7 @@ namespace Barotrauma.Abilities protected virtual void VerifyState(bool conditionsMatched, float timeSinceLastUpdate) { - DebugConsole.ThrowError($"Ability {this} does not have an implementation for VerifyState! This ability does not work in interval ability groups."); + DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}: Ability {this} does not have an implementation for VerifyState! This ability does not work in interval ability groups."); } public void ApplyAbilityEffect(AbilityObject abilityObject) @@ -128,14 +128,5 @@ namespace Barotrauma.Abilities DebugConsole.AddWarning("Instantiated " + characterAbility + " for talent " + characterAbilityGroup.CharacterTalent.DebugIdentifier); return characterAbility; } - public static AbilityFlags ParseFlagType(string flagTypeString, string debugIdentifier) - { - AbilityFlags flagType = AbilityFlags.None; - if (!Enum.TryParse(flagTypeString, true, out flagType)) - { - DebugConsole.ThrowError("Invalid flag type type \"" + flagTypeString + "\" in CharacterTalent (" + debugIdentifier + ")"); - } - return flagType; - } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveAffliction.cs new file mode 100644 index 000000000..5f56b433a --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveAffliction.cs @@ -0,0 +1,44 @@ +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityGiveAffliction : CharacterAbility + { + private readonly string afflictionId; + private readonly float strength; + private readonly string multiplyStrengthBySkill; + private readonly bool setValue; + + public CharacterAbilityGiveAffliction(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + afflictionId = abilityElement.GetAttributeString("afflictionid", abilityElement.GetAttributeString("affliction", string.Empty)); + strength = abilityElement.GetAttributeFloat("strength", 0f); + multiplyStrengthBySkill = abilityElement.GetAttributeString("multiplystrengthbyskill", string.Empty); + setValue = abilityElement.GetAttributeBool("setvalue", false); + + if (string.IsNullOrEmpty(afflictionId)) + { + DebugConsole.ThrowError("Error in CharacterAbilityGiveAffliction - affliction identifier not set."); + } + } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + if (abilityObject is IAbilityCharacter character) + { + var afflictionPrefab = AfflictionPrefab.Prefabs.Find(a => a.Identifier.Equals(afflictionId, System.StringComparison.OrdinalIgnoreCase)); + if (afflictionPrefab == null) + { + DebugConsole.ThrowError($"Error in CharacterAbilityGiveAffliction - could not find an affliction with the identifier \"{afflictionId}\"."); + return; + } + float strength = this.strength; + if (!string.IsNullOrEmpty(multiplyStrengthBySkill)) + { + strength *= Character.GetSkillLevel(multiplyStrengthBySkill); + } + character.Character.CharacterHealth.ApplyAffliction(null, afflictionPrefab.Instantiate(strength), allowStacking: !setValue); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveFlag.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveFlag.cs index 921807085..76b3960ea 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveFlag.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveFlag.cs @@ -4,12 +4,12 @@ namespace Barotrauma.Abilities { class CharacterAbilityGiveFlag : CharacterAbility { - private AbilityFlags abilityFlag; + private readonly AbilityFlags abilityFlag; // this and resistance giving should probably be moved directly to charactertalent attributes, as they don't need to interact with either ability group types public CharacterAbilityGiveFlag(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) { - abilityFlag = ParseFlagType(abilityElement.GetAttributeString("flagtype", ""), CharacterTalent.DebugIdentifier); + abilityFlag = CharacterAbilityGroup.ParseFlagType(abilityElement.GetAttributeString("flagtype", ""), CharacterTalent.DebugIdentifier); } public override void InitializeAbility(bool addingFirstTime) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMoney.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMoney.cs index d02647357..9c4dd581a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMoney.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMoney.cs @@ -7,20 +7,20 @@ namespace Barotrauma.Abilities public override bool AppliesEffectOnIntervalUpdate => true; private readonly int amount; - private readonly StatTypes scalingStatType; + private readonly string scalingStatIdentifier; public CharacterAbilityGiveMoney(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) { amount = abilityElement.GetAttributeInt("amount", 0); - scalingStatType = CharacterAbilityGroup.ParseStatType(abilityElement.GetAttributeString("scalingstattype", "None"), CharacterTalent.DebugIdentifier); + scalingStatIdentifier = abilityElement.GetAttributeString("scalingstatidentifier", string.Empty); } private void ApplyEffectSpecific(Character targetCharacter) { float multiplier = 1f; - if (scalingStatType != StatTypes.None) + if (!string.IsNullOrEmpty(scalingStatIdentifier)) { - multiplier = 0 + Character.Info.GetSavedStatValue(scalingStatType); + multiplier = 0 + Character.Info.GetSavedStatValue(StatTypes.None, scalingStatIdentifier); } targetCharacter.GiveMoney((int)(multiplier * amount)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs index dce3ed4f8..ffa6df774 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs @@ -11,7 +11,6 @@ namespace Barotrauma.Abilities private readonly float maxValue; private readonly bool targetAllies; private readonly bool removeOnDeath; - private readonly bool removeAfterRound; private readonly bool giveOnAddingFirstTime; private readonly bool setValue; @@ -22,12 +21,12 @@ namespace Barotrauma.Abilities public CharacterAbilityGivePermanentStat(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) { statIdentifier = abilityElement.GetAttributeString("statidentifier", "").ToLowerInvariant(); - statType = CharacterAbilityGroup.ParseStatType(abilityElement.GetAttributeString("stattype", ""), CharacterTalent.DebugIdentifier); + string statTypeName = abilityElement.GetAttributeString("stattype", string.Empty); + statType = string.IsNullOrEmpty(statTypeName) ? StatTypes.None : CharacterAbilityGroup.ParseStatType(statTypeName, CharacterTalent.DebugIdentifier); value = abilityElement.GetAttributeFloat("value", 0f); maxValue = abilityElement.GetAttributeFloat("maxvalue", float.MaxValue); targetAllies = abilityElement.GetAttributeBool("targetallies", false); removeOnDeath = abilityElement.GetAttributeBool("removeondeath", true); - removeAfterRound = abilityElement.GetAttributeBool("removeafterround", false); giveOnAddingFirstTime = abilityElement.GetAttributeBool("giveonaddingfirsttime", characterAbilityGroup.AbilityEffectType == AbilityEffectType.None); setValue = abilityElement.GetAttributeBool("setvalue", false); } @@ -54,11 +53,11 @@ namespace Barotrauma.Abilities { if (targetAllies) { - Character.GetFriendlyCrew(Character).ForEach(c => c?.Info.ChangeSavedStatValue(statType, value, statIdentifier, removeOnDeath, removeAfterRound, maxValue, setValue)); + Character.GetFriendlyCrew(Character).ForEach(c => c?.Info.ChangeSavedStatValue(statType, value, statIdentifier, removeOnDeath, maxValue: maxValue, setValue: setValue)); } else { - Character?.Info.ChangeSavedStatValue(statType, value, statIdentifier, removeOnDeath, removeAfterRound, maxValue, setValue); + Character?.Info.ChangeSavedStatValue(statType, value, statIdentifier, removeOnDeath, maxValue: maxValue, setValue: setValue); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyFlag.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyFlag.cs index 4ab462ccf..d9953bf23 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyFlag.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyFlag.cs @@ -5,13 +5,13 @@ namespace Barotrauma.Abilities { class CharacterAbilityModifyFlag : CharacterAbility { - private AbilityFlags abilityFlag; + private readonly AbilityFlags abilityFlag; private bool lastState; public CharacterAbilityModifyFlag(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) { - abilityFlag = ParseFlagType(abilityElement.GetAttributeString("flagtype", ""), CharacterTalent.DebugIdentifier); + abilityFlag = CharacterAbilityGroup.ParseFlagType(abilityElement.GetAttributeString("flagtype", ""), CharacterTalent.DebugIdentifier); } protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs index 5ada7de4a..7c96b3d17 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs @@ -208,5 +208,15 @@ namespace Barotrauma.Abilities return afflictions; } + + public static AbilityFlags ParseFlagType(string flagTypeString, string debugIdentifier) + { + AbilityFlags flagType = AbilityFlags.None; + if (!Enum.TryParse(flagTypeString, true, out flagType)) + { + DebugConsole.ThrowError("Invalid flag type type \"" + flagTypeString + "\" in CharacterTalent (" + debugIdentifier + ")"); + } + return flagType; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs index 92736e1d1..1a96f6f38 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs @@ -30,7 +30,7 @@ namespace Barotrauma { ConfigElement = element; - string jobIdentifier = element.GetAttributeString("jobidentifier", ""); + string jobIdentifier = element.GetAttributeString("jobidentifier", "").ToLowerInvariant(); if (string.IsNullOrEmpty(jobIdentifier)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 39810a12f..ab72e7dc2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -863,6 +863,60 @@ namespace Barotrauma }; }, isCheat: true)); + commands.Add(new Command("unlocktalents", "unlocktalents [all/[jobname]]: give the controlled characters all the talents of the specified class", (string[] args) => + { + if (Character.Controlled == null) { return; } + if (args.Length == 0 || args[0].Equals("all", StringComparison.OrdinalIgnoreCase)) + { + foreach (var talentTree in TalentTree.JobTalentTrees) + { + foreach (var subTree in talentTree.Value.TalentSubTrees) + { + foreach (var option in subTree.TalentOptionStages) + { + foreach (var talent in option.Talents) + { + Character.Controlled.GiveTalent(talent); + } + } + } + } + } + else + { + var job = JobPrefab.Prefabs.Find(jp => jp.Name != null && jp.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase)); + if (job == null) + { + ThrowError($"Failed to find the job \"{args[0]}\"."); + return; + } + if (!TalentTree.JobTalentTrees.TryGetValue(job.Identifier, out TalentTree talentTree)) + { + ThrowError($"No talents configured for the job \"{args[0]}\"."); + return; + } + foreach (var subTree in talentTree.TalentSubTrees) + { + foreach (var option in subTree.TalentOptionStages) + { + foreach (var talent in option.Talents) + { + Character.Controlled.GiveTalent(talent); + } + } + } + } + }, + () => + { + List availableArgs = new List() { "All" }; + availableArgs.AddRange(JobPrefab.Prefabs.Select(j => j.Name)); + return new string[][] + { + availableArgs.ToArray() + }; + }, isCheat: true)); + commands.Add(new Command("giveexperience", "giveexperience [amount] [character]: Give experience to character.", (string[] args) => { if (args.Length < 1) @@ -892,7 +946,7 @@ namespace Barotrauma }, isCheat: true, getValidArgs: () => { return new[] - { + { new string[] { "100" }, Character.CharacterList.Select(c => c.Name).Distinct().ToArray(), }; @@ -2041,6 +2095,7 @@ namespace Barotrauma { spawnLocation = args[^2]; if (!int.TryParse(args[^1], NumberStyles.Any, CultureInfo.InvariantCulture, out amount)) { amount = 1; } + amount = Math.Min(amount, 100); } switch (spawnLocation) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs index 90d564ef1..619a77ea3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs @@ -22,9 +22,10 @@ OnSevered = 17, OnProduceSpawned = 18, OnOpen = 19, OnClose = 20, - OnDeath = OnBroken, - OnSuccess, - OnAbility, + OnSpawn = 21, + OnSuccess = 22, + OnAbility = 23, + OnDeath = OnBroken } public enum AbilityEffectType @@ -62,7 +63,10 @@ OnItemDeconstructedInventory, OnStopTinkering, OnItemPicked, + OnGeneticMaterialCombinedOrRefined, + OnCrewGeneticMaterialCombinedOrRefined, AfterSubmarineAttacked, + OnApplyTreatment, } public enum StatTypes @@ -100,7 +104,8 @@ RepairToolStructureRepairMultiplier, RepairToolStructureDamageMultiplier, RepairToolDeattachTimeMultiplier, - MaxRepairConditionMultiplier, + MaxRepairConditionMultiplierMechanical, + MaxRepairConditionMultiplierElectrical, IncreaseFabricationQuality, GeneticMaterialRefineBonus, GeneticMaterialTaintedProbabilityReductionOnCombine, @@ -114,11 +119,9 @@ MissionMoneyGainMultiplier, ExperienceGainMultiplier, MissionExperienceGainMultiplier, - // these should be deprecated and moved to their own implementation, no sense making them share space with stat values - Coauthor, - WarriorPoetMissionRuns, - WarriorPoetEnemiesKilled, - QuickfixRepairCount, + ExtraSpecialSalesCount, + ApplyTreatmentsOnSelfFraction, + MaxAttachableCount, } public enum AbilityFlags @@ -134,6 +137,8 @@ GainSkillPastMaximum, RetainExperienceForNewCharacter, AllowSecondOrderedTarget, + PowerfulCPR, + AlwaysStayConscious, } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs index 4737eba63..d42fb1cf6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs @@ -226,11 +226,11 @@ namespace Barotrauma List potentialItems = SpawnLocation switch { SpawnLocationType.MainSub => Item.ItemList.FindAll(it => it.Submarine == Submarine.MainSub), - SpawnLocationType.MainPath => Item.ItemList.FindAll(it => it.Submarine == null && it.ParentRuin == null), - SpawnLocationType.Outpost => Item.ItemList.FindAll(it => it.Submarine != null && it.Submarine.Info.IsOutpost), - SpawnLocationType.Wreck => Item.ItemList.FindAll(it => it.Submarine != null && it.Submarine.Info.IsWreck), - SpawnLocationType.Ruin => Item.ItemList.FindAll(it => it.ParentRuin != null), - SpawnLocationType.BeaconStation => Item.ItemList.FindAll(it => it.Submarine != null && it.Submarine.Info.IsBeacon), + SpawnLocationType.MainPath => Item.ItemList.FindAll(it => it.Submarine == null), + SpawnLocationType.Outpost => Item.ItemList.FindAll(it => it.Submarine?.Info != null && it.Submarine.Info.IsOutpost), + SpawnLocationType.Wreck => Item.ItemList.FindAll(it => it.Submarine?.Info != null && it.Submarine.Info.IsWreck), + SpawnLocationType.Ruin => Item.ItemList.FindAll(it => it.Submarine?.Info != null && it.Submarine.Info.IsRuin), + SpawnLocationType.BeaconStation => Item.ItemList.FindAll(it => it.Submarine?.Info != null && it.Submarine.Info.IsBeacon), _ => throw new NotImplementedException() }; @@ -252,11 +252,11 @@ namespace Barotrauma List potentialSpawnPoints = spawnLocation switch { SpawnLocationType.MainSub => WayPoint.WayPointList.FindAll(wp => wp.Submarine == Submarine.MainSub && wp.CurrentHull != null), - SpawnLocationType.MainPath => WayPoint.WayPointList.FindAll(wp => wp.Submarine == null && wp.ParentRuin == null), - SpawnLocationType.Outpost => WayPoint.WayPointList.FindAll(wp => wp.Submarine != null && wp.CurrentHull != null && wp.Submarine.Info.IsOutpost), - SpawnLocationType.Wreck => WayPoint.WayPointList.FindAll(wp => wp.Submarine != null && wp.Submarine.Info.IsWreck), - SpawnLocationType.Ruin => WayPoint.WayPointList.FindAll(wp => wp.ParentRuin != null), - SpawnLocationType.BeaconStation => WayPoint.WayPointList.FindAll(wp => wp.Submarine != null && wp.Submarine.Info.IsBeacon), + SpawnLocationType.MainPath => WayPoint.WayPointList.FindAll(wp => wp.Submarine == null), + SpawnLocationType.Outpost => WayPoint.WayPointList.FindAll(wp => wp.Submarine?.Info != null && wp.CurrentHull != null && wp.Submarine.Info.IsOutpost), + SpawnLocationType.Wreck => WayPoint.WayPointList.FindAll(wp => wp.Submarine?.Info != null && wp.Submarine.Info.IsWreck), + SpawnLocationType.Ruin => WayPoint.WayPointList.FindAll(wp => wp.Submarine?.Info != null && wp.Submarine.Info.IsRuin), + SpawnLocationType.BeaconStation => WayPoint.WayPointList.FindAll(wp => wp.Submarine?.Info != null && wp.Submarine.Info.IsBeacon), _ => throw new NotImplementedException() }; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index ee63377eb..8fee80718 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -981,6 +981,7 @@ namespace Barotrauma return false; case SubmarineType.Wreck: case SubmarineType.BeaconStation: + case SubmarineType.Ruin: return true; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs new file mode 100644 index 000000000..ad82de75c --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/AlienRuinMission.cs @@ -0,0 +1,176 @@ +using Barotrauma.Extensions; +using Barotrauma.RuinGeneration; +using Microsoft.Xna.Framework; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + partial class AlienRuinMission : Mission + { + private readonly string[] targetItemIdentifiers; + private readonly string[] targetEnemyIdentifiers; + private readonly int minEnemyCount; + private readonly HashSet existingTargets = new HashSet(); + private readonly HashSet spawnedTargets = new HashSet(); + private readonly HashSet allTargets = new HashSet(); + + private Ruin TargetRuin { get; set; } + + public override IEnumerable SonarPositions + { + get + { + if (State == 0) + { + return allTargets.Where(t => (t is Item i && !IsItemDestroyed(i)) || (t is Character c && !IsEnemyDefeated(c))).Select(t => t.WorldPosition); + } + else + { + return Enumerable.Empty(); + } + } + } + + public AlienRuinMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) + { + targetItemIdentifiers = prefab.ConfigElement.GetAttributeStringArray("targetitems", new string[0], convertToLowerInvariant: true); + targetEnemyIdentifiers = prefab.ConfigElement.GetAttributeStringArray("targetenemies", new string[0], convertToLowerInvariant: true); + minEnemyCount = prefab.ConfigElement.GetAttributeInt("minenemycount", 0); + } + + protected override void StartMissionSpecific(Level level) + { + existingTargets.Clear(); + spawnedTargets.Clear(); + allTargets.Clear(); + if (IsClient) { return; } + TargetRuin = Level.Loaded?.Ruins?.GetRandom(randSync: Rand.RandSync.Server); + if (TargetRuin == null) + { + DebugConsole.ThrowError($"Failed to initialize an Alien Ruin mission (\"{Prefab.Identifier}\"): level contains no alien ruins"); + return; + } + if (targetItemIdentifiers.Length < 1 && targetEnemyIdentifiers.Length < 1) + { + DebugConsole.ThrowError($"Failed to initialize an Alien Ruin mission (\"{Prefab.Identifier}\"): no target identifiers set in the mission definition"); + return; + } + foreach (var item in Item.ItemList) + { + if (!targetItemIdentifiers.Contains(item.Prefab.Identifier)) { continue; } + if (item.Submarine != TargetRuin.Submarine) { continue; } + existingTargets.Add(item); + allTargets.Add(item); + } + int existingEnemyCount = 0; + foreach (var character in Character.CharacterList) + { + if (string.IsNullOrEmpty(character.SpeciesName)) { continue; } + if (!targetEnemyIdentifiers.Contains(character.SpeciesName.ToLowerInvariant())) { continue; } + if (character.Submarine != TargetRuin.Submarine) { continue; } + existingTargets.Add(character); + allTargets.Add(character); + existingEnemyCount++; + } + if (existingEnemyCount < minEnemyCount) + { + var enemyPrefabs = new HashSet(); + foreach (string identifier in targetEnemyIdentifiers) + { + var prefab = CharacterPrefab.FindBySpeciesName(identifier); + if (prefab != null) + { + enemyPrefabs.Add(prefab); + } + else + { + DebugConsole.ThrowError($"Error in an Alien Ruin mission (\"{Prefab.Identifier}\"): could not find a character prefab with the species \"{identifier}\""); + } + } + if (enemyPrefabs.None()) + { + DebugConsole.ThrowError($"Error in an Alien Ruin mission (\"{Prefab.Identifier}\"): no enemy species defined that could be used to spawn more ({minEnemyCount - existingEnemyCount}) enemies"); + return; + } + for (int i = 0; i < (minEnemyCount - existingEnemyCount); i++) + { + var prefab = enemyPrefabs.GetRandom(); + var spawnPos = TargetRuin.Submarine.GetWaypoints(false).GetRandom(w => w.CurrentHull != null)?.WorldPosition; + if (!spawnPos.HasValue) + { + DebugConsole.ThrowError($"Error in an Alien Ruin mission (\"{Prefab.Identifier}\"): no valid spawn positions could be found for the additional ({minEnemyCount - existingEnemyCount}) enemies to be spawned"); + return; + } + var newEnemy = Character.Create(prefab.Identifier, spawnPos.Value, ToolBox.RandomSeed(8), createNetworkEvent: false); + spawnedTargets.Add(newEnemy); + allTargets.Add(newEnemy); + } + } +#if DEBUG + DebugConsole.NewMessage("********** CLEAR RUIN MISSION INFO **********"); + DebugConsole.NewMessage($"Existing item targets: {existingTargets.Count - existingEnemyCount}"); + DebugConsole.NewMessage($"Existing enemy targets: {existingEnemyCount}"); + DebugConsole.NewMessage($"Spawned enemy targets: {spawnedTargets.Count}"); +#endif + } + + protected override void UpdateMissionSpecific(float deltaTime) + { + if (IsClient) { return; } + switch (State) + { + case 0: + if (!AllTargetsEliminated()) { return; } + State = 1; + break; + case 1: + if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) { return; } + State = 2; + break; + } + } + + private bool AllTargetsEliminated() + { + foreach (var target in allTargets) + { + if (target is Item targetItem) + { + if (!IsItemDestroyed(targetItem)) + { + return false; + } + } + else if (target is Character targetEnemy) + { + if (!IsEnemyDefeated(targetEnemy)) + { + return false; + } + } +#if DEBUG + else + { + DebugConsole.ThrowError($"Error in Alien Ruin mission (\"{Prefab.Identifier}\"): unexpected target of type {target?.GetType()?.ToString()}"); + } +#endif + } + return true; + } + + private bool IsItemDestroyed(Item item) => item == null || item.Removed || item.Condition <= 0.0f; + + private bool IsEnemyDefeated(Character enemy) => enemy == null ||enemy.Removed || enemy.IsDead; + + public override void End() + { + if (AllTargetsEliminated()) + { + GiveReward(); + completed = true; + } + failed = !completed && state > 0; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs index 023e1e860..d51ccd3fd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs @@ -198,55 +198,14 @@ namespace Barotrauma if (requiredDeliveryAmount <= 0.0f) { requiredDeliveryAmount = 1.0f; } } - private ItemPrefab FindItemPrefab(XElement element) - { - ItemPrefab itemPrefab; - if (element.Attribute("name") != null) - { - DebugConsole.ThrowError("Error in cargo mission \"" + Name + "\" - use item identifiers instead of names to configure the items."); - string itemName = element.GetAttributeString("name", ""); - itemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab; - if (itemPrefab == null) - { - DebugConsole.ThrowError("Couldn't spawn item for cargo mission: item prefab \"" + itemName + "\" not found"); - } - } - else - { - string itemIdentifier = element.GetAttributeString("identifier", ""); - itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab; - if (itemPrefab == null) - { - DebugConsole.ThrowError("Couldn't spawn item for cargo mission: item prefab \"" + itemIdentifier + "\" not found"); - } - } - return itemPrefab; - } - - private void LoadItemAsChild(XElement element, Item parent) { ItemPrefab itemPrefab = FindItemPrefab(element); - WayPoint cargoSpawnPos = WayPoint.GetRandom(SpawnType.Cargo, null, Submarine.MainSub, useSyncedRand: true); - if (cargoSpawnPos == null) - { - DebugConsole.ThrowError("Couldn't spawn items for cargo mission, cargo spawnpoint not found"); - return; - } + Vector2? position = GetCargoSpawnPosition(itemPrefab, out Submarine cargoRoomSub); + if (!position.HasValue) { return; } - var cargoRoom = cargoSpawnPos.CurrentHull; - if (cargoRoom == null) - { - DebugConsole.ThrowError("A waypoint marked as Cargo must be placed inside a room!"); - return; - } - - Vector2 position = new Vector2( - cargoSpawnPos.Position.X + Rand.Range(-20.0f, 20.0f, Rand.RandSync.Server), - cargoRoom.Rect.Y - cargoRoom.Rect.Height + itemPrefab.Size.Y / 2); - - var item = new Item(itemPrefab, position, cargoRoom.Submarine) + var item = new Item(itemPrefab, position.Value, cargoRoomSub) { SpawnedInOutpost = true, AllowStealing = false diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index d6adbf7b5..bfa584f9c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -27,7 +27,7 @@ namespace Barotrauma state = value; TryTriggerEvents(state); #if SERVER - GameMain.Server?.UpdateMissionState(this, state); + GameMain.Server?.UpdateMissionState(this); #endif ShowMessage(State); } @@ -347,7 +347,10 @@ namespace Barotrauma if (!(GameMain.GameSession.GameMode is CampaignMode campaign)) { return; } int reward = GetReward(Submarine.MainSub); - float baseExperienceGain = reward * 0.15f; + float baseExperienceGain = reward * 0.1f; + + float difficultyMultiplier = 1 + level.Difficulty / 100f; + baseExperienceGain *= difficultyMultiplier; IEnumerable crewCharacters = GameSession.GetSessionCrewCharacters(); @@ -471,5 +474,55 @@ namespace Barotrauma return spawnedCharacter; } + + protected ItemPrefab FindItemPrefab(XElement element) + { + ItemPrefab itemPrefab; + if (element.Attribute("name") != null) + { + DebugConsole.ThrowError($"Error in mission \"{Name}\" - use item identifiers instead of names to configure the items"); + string itemName = element.GetAttributeString("name", ""); + itemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab; + if (itemPrefab == null) + { + DebugConsole.ThrowError($"Couldn't spawn item for mission \"{Name}\": item prefab \"{itemName}\" not found"); + } + } + else + { + string itemIdentifier = element.GetAttributeString("identifier", ""); + itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab; + if (itemPrefab == null) + { + DebugConsole.ThrowError($"Couldn't spawn item for mission \"{Name}\": item prefab \"{itemIdentifier}\" not found"); + } + } + return itemPrefab; + } + + protected Vector2? GetCargoSpawnPosition(ItemPrefab itemPrefab, out Submarine cargoRoomSub) + { + cargoRoomSub = null; + + WayPoint cargoSpawnPos = WayPoint.GetRandom(SpawnType.Cargo, null, Submarine.MainSub, useSyncedRand: true); + if (cargoSpawnPos == null) + { + DebugConsole.ThrowError($"Couldn't spawn items for mission \"{Name}\": no waypoints marked as Cargo were found"); + return null; + } + + var cargoRoom = cargoSpawnPos.CurrentHull; + if (cargoRoom == null) + { + DebugConsole.ThrowError($"Couldn't spawn items for mission \"{Name}\": waypoints marked as Cargo must be placed inside a room"); + return null; + } + + cargoRoomSub = cargoRoom.Submarine; + + return new Vector2( + cargoSpawnPos.Position.X + Rand.Range(-20.0f, 20.0f, Rand.RandSync.Server), + cargoRoom.Rect.Y - cargoRoom.Rect.Height + itemPrefab.Size.Y / 2); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs index 80be751bf..0fa150677 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs @@ -22,7 +22,9 @@ namespace Barotrauma Escort = 0x100, Pirate = 0x200, GoTo = 0x400, - All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat | AbandonedOutpost | Escort | Pirate | GoTo + ScanAlienRuins = 0x800, + ClearAlienRuins = 0x1000, + All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat | AbandonedOutpost | Escort | Pirate | GoTo | ScanAlienRuins | ClearAlienRuins } partial class MissionPrefab @@ -40,7 +42,9 @@ namespace Barotrauma { MissionType.AbandonedOutpost, typeof(AbandonedOutpostMission) }, { MissionType.Escort, typeof(EscortMission) }, { MissionType.Pirate, typeof(PirateMission) }, - { MissionType.GoTo, typeof(GoToMission) } + { MissionType.GoTo, typeof(GoToMission) }, + { MissionType.ScanAlienRuins, typeof(ScanMission) }, + { MissionType.ClearAlienRuins, typeof(AlienRuinMission) } }; public static readonly Dictionary PvPMissionClasses = new Dictionary() { @@ -372,6 +376,11 @@ namespace Barotrauma var connection = from.Connections.Find(c => c.Locations.Contains(from) && c.Locations.Contains(to)); if (connection?.LevelData == null || !connection.LevelData.HasBeaconStation || connection.LevelData.IsBeaconActive) { return false; } } + else if (Type == MissionType.ScanAlienRuins || Type == MissionType.ClearAlienRuins) + { + var connection = from.Connections.Find(c => c.Locations.Contains(from) && c.Locations.Contains(to)); + if (connection?.LevelData == null || connection.LevelData.GenerationParams.RuinCount < 1) { return false; } + } return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs index e2b586e73..5f628660c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs @@ -126,12 +126,12 @@ namespace Barotrauma item = suitableItems.FirstOrDefault(it => Vector2.DistanceSquared(it.WorldPosition, position) < 1000.0f); break; case Level.PositionType.Ruin: - item = suitableItems.FirstOrDefault(it => it.ParentRuin != null && it.ParentRuin.Area.Contains(position)); - break; case Level.PositionType.Wreck: foreach (Item it in suitableItems) { - if (it.Submarine == null || it.Submarine.Info.Type != SubmarineType.Wreck) { continue; } + if (it.Submarine?.Info == null) { continue; } + if (spawnPositionType == Level.PositionType.Ruin && it.Submarine.Info.Type != SubmarineType.Ruin) { continue; } + if (spawnPositionType == Level.PositionType.Wreck && it.Submarine.Info.Type != SubmarineType.Wreck) { continue; } Rectangle worldBorders = it.Submarine.Borders; worldBorders.Location += it.Submarine.WorldPosition.ToPoint(); if (Submarine.RectContains(worldBorders, it.WorldPosition)) @@ -178,10 +178,10 @@ namespace Barotrauma { case Level.PositionType.Cave: case Level.PositionType.MainPath: - if (it.Submarine != null || it.ParentRuin != null) { continue; } + if (it.Submarine != null) { continue; } break; case Level.PositionType.Ruin: - if (it.ParentRuin == null) { continue; } + if (it.Submarine?.Info == null || !it.Submarine.Info.IsRuin) { continue; } break; case Level.PositionType.Wreck: if (it.Submarine == null || it.Submarine.Info.Type != SubmarineType.Wreck) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs new file mode 100644 index 000000000..410e7da37 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/ScanMission.cs @@ -0,0 +1,258 @@ +using Barotrauma.Extensions; +using Barotrauma.Items.Components; +using Barotrauma.RuinGeneration; +using Microsoft.Xna.Framework; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + partial class ScanMission : Mission + { + private readonly XElement itemConfig; + private readonly List startingItems = new List(); + private readonly List scanners = new List(); + private readonly Dictionary parentInventoryIDs = new Dictionary(); + private readonly Dictionary parentItemContainerIndices = new Dictionary(); + private readonly int targetsToScan; + private readonly Dictionary scanTargets = new Dictionary(); + private readonly HashSet newTargetsScanned = new HashSet(); + private readonly float minTargetDistance, minTargetDistanceSquared; + + + private Ruin TargetRuin { get; set; } + + private bool AllTargetsScanned + { + get + { + return scanTargets.Any() && scanTargets.All(kvp => kvp.Value); + } + } + + public override IEnumerable SonarPositions + { + get + { + if (State > 0) + { + return Enumerable.Empty(); + } + else if (scanTargets.Any()) + { + return scanTargets + .Where(kvp => !kvp.Value) + .Select(kvp => kvp.Key.WorldPosition); + } + else + { + return Enumerable.Empty(); + } + + } + } + + public ScanMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) + { + itemConfig = prefab.ConfigElement.Element("Items"); + targetsToScan = prefab.ConfigElement.GetAttributeInt("targets", 1); + minTargetDistance = prefab.ConfigElement.GetAttributeFloat("mintargetdistance", 0.0f); + minTargetDistanceSquared = minTargetDistance * minTargetDistance; + } + + protected override void StartMissionSpecific(Level level) + { + Reset(); + + if (IsClient) { return; } + + if (itemConfig == null) + { + DebugConsole.ThrowError("Failed to initialize a Scan mission: item config is not set"); + return; + } + + foreach (var element in itemConfig.Elements()) + { + LoadItem(element, null); + } + GetScanners(); + + TargetRuin = Level.Loaded?.Ruins?.GetRandom(randSync: Rand.RandSync.Server); + if (TargetRuin == null) + { + DebugConsole.ThrowError("Failed to initialize a Scan mission: level contains no alien ruins"); + return; + } + + var availableWaypoints = TargetRuin.Submarine.GetWaypoints(false); + availableWaypoints.RemoveAll(wp => wp.CurrentHull == null); + if (availableWaypoints.Count < targetsToScan) + { + DebugConsole.ThrowError($"Failed to initialize a Scan mission: target ruin has less waypoints than required as scan targets ({availableWaypoints.Count} < {targetsToScan})"); + return; + } + for (int i = 0; i < targetsToScan; i++) + { + var selectedWaypoint = availableWaypoints.GetRandom(randSync: Rand.RandSync.Server); + scanTargets.Add(selectedWaypoint, false); + availableWaypoints.Remove(selectedWaypoint); + if (i < (targetsToScan - 1)) + { + availableWaypoints.RemoveAll(wp => wp.CurrentHull == selectedWaypoint.CurrentHull); + availableWaypoints.RemoveAll(wp => Vector2.DistanceSquared(wp.WorldPosition, selectedWaypoint.WorldPosition) < minTargetDistanceSquared); + if (availableWaypoints.None()) + { + DebugConsole.ThrowError($"Error initializing a Scan mission: not enough targets available to reach the required scan target count (current targets: {scanTargets.Count}, required targets: {targetsToScan})"); + break; + } + } + } + } + + private void Reset() + { + startingItems.Clear(); + parentInventoryIDs.Clear(); + parentItemContainerIndices.Clear(); + scanners.Clear(); + TargetRuin = null; + scanTargets.Clear(); + } + + private void LoadItem(XElement element, Item parent) + { + var itemPrefab = FindItemPrefab(element); + Vector2? position = GetCargoSpawnPosition(itemPrefab, out Submarine cargoRoomSub); + if (!position.HasValue) { return; } + var item = new Item(itemPrefab, position.Value, cargoRoomSub); + item.FindHull(); + startingItems.Add(item); + if (parent?.GetComponent() is ItemContainer itemContainer) + { + parentInventoryIDs.Add(item, parent.ID); + parentItemContainerIndices.Add(item, (byte)parent.GetComponentIndex(itemContainer)); + parent.Combine(item, user: null); + } + foreach (XElement subElement in element.Elements()) + { + int amount = subElement.GetAttributeInt("amount", 1); + for (int i = 0; i < amount; i++) + { + LoadItem(subElement, item); + } + } + } + + private void GetScanners() + { + foreach (var startingItem in startingItems) + { + if (startingItem.GetComponent() is Scanner scanner) + { + scanner.OnScanStarted += OnScanStarted; + if (!IsClient) + { + scanner.OnScanCompleted += OnScanCompleted; + } + scanners.Add(scanner); + } + } + } + + private void OnScanStarted(Scanner scanner) + { + float scanRadiusSquared = scanner.ScanRadius * scanner.ScanRadius; + foreach (var kvp in scanTargets) + { + if (!IsValidScanPosition(scanner, kvp, scanRadiusSquared)) { continue; } + scanner.DisplayProgressBar = true; + break; + } + } + + private void OnScanCompleted(Scanner scanner) + { + if (IsClient) { return; } + newTargetsScanned.Clear(); + float scanRadiusSquared = scanner.ScanRadius * scanner.ScanRadius; + foreach (var kvp in scanTargets) + { + if (!IsValidScanPosition(scanner, kvp, scanRadiusSquared)) { continue; } + newTargetsScanned.Add(kvp.Key); + } + foreach (var wp in newTargetsScanned) + { + scanTargets[wp] = true; + } +#if SERVER + // Server should make sure that the clients' scan target status is in-sync + GameMain.Server?.UpdateMissionState(this); +#endif + } + + private bool IsValidScanPosition(Scanner scanner, KeyValuePair scanStatus, float scanRadiusSquared) + { + if (scanStatus.Value) { return false; } + if (scanStatus.Key.Submarine != scanner.Item.Submarine) { return false; } + if (Vector2.DistanceSquared(scanStatus.Key.WorldPosition, scanner.Item.WorldPosition) > scanRadiusSquared) { return false; } + return true; + } + + protected override void UpdateMissionSpecific(float deltaTime) + { + if (IsClient) { return; } + switch (State) + { + case 0: + if (!AllTargetsScanned) { return; } + State = 1; + break; + case 1: + if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) { return; } + State = 2; + break; + } + } + + public override void End() + { + if (AllTargetsScanned && AllScannersReturned()) + { + GiveReward(); + completed = true; + } + foreach (var scanner in scanners) + { + if (scanner.Item != null && !scanner.Item.Removed) + { + scanner.OnScanStarted -= OnScanStarted; + scanner.OnScanCompleted -= OnScanCompleted; + scanner.Item.Remove(); + } + } + Reset(); + failed = !completed && state > 0; + + bool AllScannersReturned() + { + foreach (var scanner in scanners) + { + if (scanner?.Item == null || scanner.Item.Removed) { return false; } + var owner = scanner.Item.GetRootInventoryOwner(); + if (owner.Submarine != null && owner.Submarine.Info.Type == SubmarineType.Player) + { + continue; + } + else if (owner is Character c && c.Info != null && GameMain.GameSession.CrewManager.CharacterInfos.Contains(c.Info)) + { + continue; + } + return false; + } + return true; + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs index 3edb99bdd..9b52a0651 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs @@ -285,11 +285,10 @@ namespace Barotrauma spawnPos = chosenPosition.Position.ToVector2(); if (chosenPosition.Submarine != null || chosenPosition.Ruin != null) { - var spawnPoint = WayPoint.GetRandom(SpawnType.Enemy, sub: chosenPosition.Submarine, ruin: chosenPosition.Ruin, useSyncedRand: false); + var spawnPoint = WayPoint.GetRandom(SpawnType.Enemy, sub: chosenPosition.Submarine ?? chosenPosition.Ruin?.Submarine, useSyncedRand: false); if (spawnPoint != null) { - System.Diagnostics.Debug.Assert(spawnPoint.Submarine == chosenPosition.Submarine); - System.Diagnostics.Debug.Assert(spawnPoint.ParentRuin == chosenPosition.Ruin); + System.Diagnostics.Debug.Assert(spawnPoint.Submarine == (chosenPosition.Submarine ?? chosenPosition.Ruin?.Submarine)); spawnPos = spawnPoint.WorldPosition; } else diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs index 127e56d71..dbff41f5b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs @@ -22,14 +22,17 @@ namespace Barotrauma Place(subs); subs.ForEach(s => s.Info.InitialSuppliesSpawned = true); } - + foreach (var sub in Submarine.Loaded) { - if (sub.Info.Type == SubmarineType.Wreck || - sub.Info.Type == SubmarineType.BeaconStation) + if (sub.Info.Type == SubmarineType.Player || + sub.Info.Type == SubmarineType.Outpost || + sub.Info.Type == SubmarineType.OutpostModule || + sub.Info.Type == SubmarineType.EnemySubmarine) { - Place(sub.ToEnumerable()); + continue; } + Place(sub.ToEnumerable()); } if (Level.Loaded?.StartOutpost != null && Level.Loaded.Type == LevelData.LevelType.Outpost) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs index 40479d07b..56915b462 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs @@ -1139,8 +1139,8 @@ namespace Barotrauma if (PlayerCharacterCustomization != null) { playerElement.SetAttributeValue("headindex", PlayerCharacterCustomization.HeadSpriteId); - playerElement.SetAttributeValue("gender", PlayerCharacterCustomization.gender); - playerElement.SetAttributeValue("race", PlayerCharacterCustomization.race); + if (PlayerCharacterCustomization.gender != Gender.None) { playerElement.SetAttributeValue("gender", PlayerCharacterCustomization.gender); } + if (PlayerCharacterCustomization.race != Race.None) { playerElement.SetAttributeValue("race", PlayerCharacterCustomization.race); } playerElement.SetAttributeValue("hairindex", PlayerCharacterCustomization.HairIndex); playerElement.SetAttributeValue("beardindex", PlayerCharacterCustomization.BeardIndex); playerElement.SetAttributeValue("moustacheindex", PlayerCharacterCustomization.MoustacheIndex); @@ -1269,7 +1269,7 @@ namespace Barotrauma playerName = playerElement.GetAttributeString("name", playerName); int head = playerElement.GetAttributeInt("headindex", -1); Enum.TryParse(playerElement.GetAttributeString("gender", "none"), true, out Gender gender); - Enum.TryParse(playerElement.GetAttributeString("race", "white"), true, out Race race); + Enum.TryParse(playerElement.GetAttributeString("race", "none"), true, out Race race); int hair = playerElement.GetAttributeInt("hairindex", -1); int beard = playerElement.GetAttributeInt("beardindex", -1); int moustache = playerElement.GetAttributeInt("moustacheindex", -1); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs index a4061293b..22f679994 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs @@ -87,7 +87,9 @@ namespace Barotrauma continue; } - Entity.Spawner?.AddToSpawnQueue(itemPrefab, this, ignoreLimbSlots: subElement.GetAttributeBool("forcetoslot", false)); + string slotString = subElement.GetAttributeString("slot", "None"); + InvSlotType slot = Enum.TryParse(slotString, ignoreCase: true, out InvSlotType s) ? s : InvSlotType.None; + Entity.Spawner?.AddToSpawnQueue(itemPrefab, this, ignoreLimbSlots: subElement.GetAttributeBool("forcetoslot", false), slot: slot); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index 942985449..d14b487f5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -79,6 +79,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(false, false, description: "Use the hand rotation instead of torso rotation for the item hold angle. Enable this if you want the item just to follow with the arm when not aiming instead of forcing the arm to a hold pose.")] + public bool UseHandRotationForHoldAngle + { + get; + set; + } + [Serialize(false, false, description: "Can the item be attached to walls.")] public bool Attachable { @@ -93,6 +100,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(false, false, description: "Can the item only be attached in limited amount? Uses permanent stat values to check for legibility.")] + public bool LimitedAttachable + { + get; + set; + } + [Serialize(false, false, description: "Should the item be attached to a wall by default when it's placed in the submarine editor.")] public bool AttachedByDefault { @@ -154,7 +168,8 @@ namespace Barotrauma.Items.Components BodyType = BodyType.Dynamic, CollidesWith = Physics.CollisionCharacter, CollisionCategories = Physics.CollisionItemBlocking, - Enabled = false + Enabled = false, + UserData = "Holdable.Pusher" }; Pusher.FarseerBody.OnCollision += OnPusherCollision; Pusher.FarseerBody.FixedRotation = false; @@ -205,7 +220,6 @@ namespace Barotrauma.Items.Components } } } - characterUsable = element.GetAttributeBool("characterusable", true); } @@ -247,6 +261,7 @@ namespace Barotrauma.Items.Components private void Drop(bool dropConnectedWires, Character dropper) { + GetRope()?.Snap(); if (dropConnectedWires) { DropConnectedWires(dropper); @@ -558,6 +573,15 @@ namespace Barotrauma.Items.Components PickKey = InputType.Select; } + public override void ParseMsg() + { + base.ParseMsg(); + if (Attachable) + { + prevMsg = DisplayMsg; + } + } + public override bool Use(float deltaTime, Character character = null) { if (!attachable || item.body == null) { return character == null || (character.IsKeyDown(InputType.Aim) && characterUsable); } @@ -567,6 +591,25 @@ namespace Barotrauma.Items.Components if (!character.IsKeyDown(InputType.Aim)) { return false; } if (!CanBeAttached(character)) { return false; } + if (LimitedAttachable) + { + if (character?.Info == null) + { + DebugConsole.AddWarning("Character without CharacterInfo attempting to attach a limited attachable item!"); + return false; + } + int maxAttachableCount = (int)character.Info.GetSavedStatValue(StatTypes.MaxAttachableCount, item.Prefab.Identifier); + int currentlyAttachedCount = Item.ItemList.Count( + i => i.Submarine == item.Submarine && i.GetComponent() is Holdable holdable && holdable.Attached && i.Prefab.Identifier == item.prefab.Identifier); + if (currentlyAttachedCount >= maxAttachableCount) + { +#if CLIENT + GUI.AddMessage($"{TextManager.Get("itemmsgtotalnumberlimited")} ({currentlyAttachedCount}/{maxAttachableCount})", Color.Red); +#endif + return false; + } + } + if (GameMain.NetworkMember != null) { if (character != Character.Controlled) @@ -666,6 +709,20 @@ namespace Barotrauma.Items.Components Update(deltaTime, cam); } + public Rope GetRope() + { + var rangedWeapon = Item.GetComponent(); + if (rangedWeapon != null) + { + var lastProjectile = rangedWeapon.LastProjectile; + if (lastProjectile != null) + { + return lastProjectile.Item.GetComponent(); + } + } + return null; + } + public override void Update(float deltaTime, Camera cam) { if (attachTargetCell != null) @@ -720,9 +777,18 @@ namespace Barotrauma.Items.Components scaledHandlePos[1] = handlePos[1] * item.Scale; bool aim = picker.IsKeyDown(InputType.Aim) && aimPos != Vector2.Zero && picker.CanAim; picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, holdPos + swing, aimPos + swing, aim, holdAngle); + if (!aim) + { + var rope = GetRope(); + if (rope != null && rope.SnapWhenNotAimed) + { + rope.Snap(); + } + } } else { + GetRope()?.Snap(); Limb equipLimb = null; if (picker.Inventory.IsInLimbSlot(item, InvSlotType.Headset) || picker.Inventory.IsInLimbSlot(item, InvSlotType.Head)) { @@ -792,7 +858,7 @@ namespace Barotrauma.Items.Components attachTargetCell = null; if (Pusher != null) { - GameMain.World.Remove(Pusher.FarseerBody); + Pusher.Remove(); Pusher = null; } body = null; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs index d362beb6f..8fcf6d265 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/IdCard.cs @@ -31,7 +31,7 @@ namespace Barotrauma.Items.Components public void Initialize(CharacterInfo info) { - if (info == null) return; + if (info == null) { return; } if (info.Job?.Prefab != null) { @@ -42,20 +42,22 @@ namespace Barotrauma.Items.Components 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 == null) { return; } + + if (info.HasGenders) { item.AddTag($"gender:{head.gender.ToString().ToLowerInvariant()}"); } + if (info.HasRaces) { item.AddTag($"race:{head.race}"); } + item.AddTag($"headspriteid:{info.HeadSpriteId}"); + item.AddTag($"hairindex:{head.HairIndex}"); + item.AddTag($"beardindex:{head.BeardIndex}"); + item.AddTag($"moustacheindex:{head.MoustacheIndex}"); + item.AddTag($"faceattachmentindex:{head.FaceAttachmentIndex}"); + item.AddTag($"haircolor:{head.HairColor.ToStringHex()}"); + item.AddTag($"facialhaircolor:{head.FacialHairColor.ToStringHex()}"); + item.AddTag($"skincolor:{head.SkinColor.ToStringHex()}"); - if (head.SheetIndex != null) - { - item.AddTag("sheetindex:" + head.SheetIndex.Value.X + ";" + head.SheetIndex.Value.Y); - } + if (head.SheetIndex != null) + { + item.AddTag($"sheetindex:{head.SheetIndex.Value.X};{head.SheetIndex.Value.Y}"); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index cb96a5935..cce65f786 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -50,6 +50,16 @@ namespace Barotrauma.Items.Components set; } + [Editable, Serialize(true, false)] + public bool Swing { get; set; } + + [Editable, Serialize("2.0, 0.0", false)] + public Vector2 SwingPos { get; set; } + + [Editable, Serialize("3.0, -1.0", false)] + public Vector2 SwingForce { get; set; } + + /// /// Defines items that boost the weapon functionality, like battery cell for stun batons. /// @@ -67,8 +77,6 @@ namespace Barotrauma.Items.Components }; } item.IsShootable = true; - // TODO: should define this in xml if we have melee weapons that don't require aim to use - item.RequireAimToUse = true; PreferredContainedItems = element.GetAttributeStringArray("preferredcontaineditems", new string[0], convertToLowerInvariant: true); } @@ -82,6 +90,9 @@ namespace Barotrauma.Items.Components public override bool Use(float deltaTime, Character character = null) { if (character == null || reloadTimer > 0.0f) { return false; } +#if CLIENT + if (!Item.RequireAimToUse && character.IsPlayer && (GUI.MouseOn != null || character.Inventory.visualSlots.Any(s => s.MouseOn()) || Inventory.DraggingItems.Any())) { return false; } +#endif if (Item.RequireAimToUse && !character.IsKeyDown(InputType.Aim) || hitting) { return false; } //don't allow hitting if the character is already hitting with another weapon @@ -94,7 +105,7 @@ namespace Barotrauma.Items.Components SetUser(character); - if (hitPos < MathHelper.PiOver4) { return false; } + if (Item.RequireAimToUse && hitPos < MathHelper.PiOver4) { return false; } ActivateNearbySleepingCharacters(); reloadTimer = reload / (1 + character.GetStatValue(StatTypes.MeleeAttackSpeed)); @@ -105,20 +116,28 @@ namespace Barotrauma.Items.Components item.body.FarseerBody.IsBullet = true; item.body.PhysEnabled = true; - if (!character.AnimController.InWater) + if (Swing && !character.AnimController.InWater) { foreach (Limb l in character.AnimController.Limbs) { if (l.IsSevered) { continue; } - if (l.type == LimbType.LeftFoot || l.type == LimbType.LeftThigh || l.type == LimbType.LeftLeg) { continue; } - if (l.type == LimbType.Head || l.type == LimbType.Torso) + Vector2 force = new Vector2(character.AnimController.Dir * SwingForce.X, SwingForce.Y) * l.Mass; + switch (l.type) { - l.body.ApplyLinearImpulse(new Vector2(character.AnimController.Dir * 7.0f, -4.0f)); + case LimbType.Torso: + force *= 2; + break; + case LimbType.Legs: + case LimbType.LeftFoot: + case LimbType.LeftThigh: + case LimbType.LeftLeg: + case LimbType.RightFoot: + case LimbType.RightThigh: + case LimbType.RightLeg: + force = Vector2.Zero; + break; } - else - { - l.body.ApplyLinearImpulse(new Vector2(character.AnimController.Dir * 5.0f, -2.0f)); - } + l.body.ApplyLinearImpulse(force); } } @@ -154,9 +173,16 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { - if (!item.body.Enabled) { impactQueue.Clear(); return; } - if (picker == null && !picker.HeldItems.Contains(item)) { impactQueue.Clear(); IsActive = false; } - + if (!item.body.Enabled) + { + impactQueue.Clear(); + return; + } + if (picker == null && !picker.HeldItems.Contains(item)) + { + impactQueue.Clear(); + IsActive = false; + } while (impactQueue.Count > 0) { var impact = impactQueue.Dequeue(); @@ -164,37 +190,47 @@ namespace Barotrauma.Items.Components } //in case handling the impact does something to the picker if (picker == null) { return; } - reloadTimer -= deltaTime; - if (reloadTimer < 0) { reloadTimer = 0; } - - if (!picker.IsKeyDown(InputType.Aim) && !hitting) { hitPos = 0.0f; } - + if (reloadTimer < 0) + { + reloadTimer = 0; + } + if (!picker.IsKeyDown(InputType.Aim) && !hitting) + { + hitPos = 0.0f; + } ApplyStatusEffects(ActionType.OnActive, deltaTime, picker); - - if (item.body.Dir != picker.AnimController.Dir) { item.FlipX(relativeToSub: false); } - + if (item.body.Dir != picker.AnimController.Dir) + { + item.FlipX(relativeToSub: false); + } AnimController ac = picker.AnimController; - - //TODO: refactor the hitting logic (get rid of the magic numbers, make it possible to use different kinds of animations for different items) if (!hitting) { - bool aim = picker.AllowInput && picker.IsKeyDown(InputType.Aim) && reloadTimer <= 0 && picker.CanAim; + bool aim = item.RequireAimToUse && picker.AllowInput && picker.IsKeyDown(InputType.Aim) && reloadTimer <= 0 && picker.CanAim; if (aim) { hitPos = MathUtils.WrapAnglePi(Math.Min(hitPos + deltaTime * 5f, MathHelper.PiOver4)); - ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, false, hitPos, holdAngle + hitPos, aimingMelee: true); + ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, hitPos, holdAngle + hitPos, aimMelee: true); } else { hitPos = 0; - ac.HoldItem(deltaTime, item, handlePos, holdPos, Vector2.Zero, false, holdAngle); + ac.HoldItem(deltaTime, item, handlePos, holdPos, Vector2.Zero, aim: false, holdAngle); } } else { + // TODO: We might want to make this configurable hitPos = MathUtils.WrapAnglePi(hitPos - deltaTime * 15f); - ac.HoldItem(deltaTime, item, handlePos, new Vector2(2, 0), Vector2.Zero, false, hitPos, holdAngle + hitPos); // aimPos not used -> zero (new Vector2(-0.3f, 0.2f)), holdPos new Vector2(0.6f, -0.1f) + if (Swing) + { + ac.HoldItem(deltaTime, item, handlePos, SwingPos, Vector2.Zero, aim: false, hitPos, holdAngle + hitPos); + } + else + { + ac.HoldItem(deltaTime, item, handlePos, holdPos, Vector2.Zero, aim: false, holdAngle); + } if (hitPos < -MathHelper.PiOver2) { RestoreCollision(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs index 60c128e6a..ee11e6001 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs @@ -1,7 +1,6 @@ using Barotrauma.Abilities; using Barotrauma.Networking; using FarseerPhysics; -using FarseerPhysics.Collision; using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using System; @@ -80,6 +79,9 @@ namespace Barotrauma.Items.Components } } + + public Projectile LastProjectile { get; private set; } + private float currentChargeTime; private bool tryingToCharge; @@ -196,6 +198,7 @@ namespace Barotrauma.Items.Components 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); + LastProjectile?.Item.GetComponent()?.Snap(); float damageMultiplier = 1f + item.GetQualityModifier(Quality.StatType.AttackMultiplier); projectile.Shoot(character, character.AnimController.AimSourceSimPos, barrelPos, rotation + spread, ignoredBodies: limbBodies.ToList(), createNetworkEvent: false, damageMultiplier); projectile.Item.GetComponent()?.Attach(Item, projectile.Item); @@ -206,6 +209,7 @@ namespace Barotrauma.Items.Components projectile.Item.body.ApplyTorque(projectile.Item.body.Mass * degreeOfFailure * Rand.Range(-10.0f, 10.0f)); Item.RemoveContained(projectile.Item); } + LastProjectile = projectile; } LaunchProjSpecific(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs index 9dbf4dd16..66b0e88f0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs @@ -123,18 +123,18 @@ namespace Barotrauma.Items.Components if (aim) { throwPos = MathUtils.WrapAnglePi(System.Math.Min(throwPos + deltaTime * 5.0f, MathHelper.PiOver2)); - ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, false, throwPos); + ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, throwPos); } else { throwPos = 0; - ac.HoldItem(deltaTime, item, handlePos, holdPos, Vector2.Zero, false, holdAngle); + ac.HoldItem(deltaTime, item, handlePos, holdPos, Vector2.Zero, aim: false, holdAngle); } } else { throwPos = MathUtils.WrapAnglePi(throwPos - deltaTime * 15.0f); - ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, false, throwPos); + ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, throwPos); if (throwPos < 0) { @@ -169,8 +169,8 @@ namespace Barotrauma.Items.Components item.body.FarseerBody.IsBullet = true; midAir = true; - ac.GetLimb(LimbType.Head).body.ApplyLinearImpulse(throwVector * 10.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); - ac.GetLimb(LimbType.Torso).body.ApplyLinearImpulse(throwVector * 10.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + ac.GetLimb(LimbType.Head)?.body.ApplyLinearImpulse(throwVector * 10.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + ac.GetLimb(LimbType.Torso)?.body.ApplyLinearImpulse(throwVector * 10.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); Limb rightHand = ac.GetLimb(LimbType.RightHand); item.body.AngularVelocity = rightHand.body.AngularVelocity; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index c146ae67e..be0b65af9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -767,7 +767,7 @@ namespace Barotrauma.Items.Components } } - public void ApplyStatusEffects(ActionType type, float deltaTime, Character character = null, Limb targetLimb = null, Entity useTarget = null, Character user = null, Vector2? worldPosition = null) + public void ApplyStatusEffects(ActionType type, float deltaTime, Character character = null, Limb targetLimb = null, Entity useTarget = null, Character user = null, Vector2? worldPosition = null, float applyOnUserFraction = 0.0f) { if (statusEffectLists == null) { return; } @@ -779,7 +779,13 @@ namespace Barotrauma.Items.Components { if (broken && !effect.AllowWhenBroken && effect.type != ActionType.OnBroken) { continue; } if (user != null) { effect.SetUser(user); } - item.ApplyStatusEffect(effect, type, deltaTime, character, targetLimb, useTarget, false, false, worldPosition); + item.ApplyStatusEffect(effect, type, deltaTime, character, targetLimb, useTarget, isNetworkEvent: false, checkCondition: false, worldPosition); + if (user != null && applyOnUserFraction > 0.0f && effect.HasTargetType(StatusEffect.TargetType.Character)) + { + effect.AfflictionMultiplier = applyOnUserFraction; + item.ApplyStatusEffect(effect, type, deltaTime, user, targetLimb == null ? null : user.AnimController.GetLimb(targetLimb.type), useTarget, false, false, worldPosition); + effect.AfflictionMultiplier = 1.0f; + } reducesCondition |= effect.ReducesItemCondition(); } //if any of the effects reduce the item's condition, set the user for OnBroken effects as well @@ -959,7 +965,7 @@ namespace Barotrauma.Items.Components } } - public void ParseMsg() + public virtual void ParseMsg() { string msg = TextManager.Get(Msg, true); if (msg != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs index e779c75ba..5efe930de 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs @@ -220,6 +220,12 @@ namespace Barotrauma.Items.Components if (targetItem == otherItem) { continue; } if (deconstructProduct.RequiredOtherItem.Any(r => otherItem.HasTag(r) || r.Equals(otherItem.Prefab.Identifier, StringComparison.OrdinalIgnoreCase))) { + user.CheckTalents(AbilityEffectType.OnGeneticMaterialCombinedOrRefined); + foreach (Character character in Character.GetFriendlyCrew(user)) + { + character.CheckTalents(AbilityEffectType.OnCrewGeneticMaterialCombinedOrRefined); + } + var geneticMaterial1 = targetItem.GetComponent(); var geneticMaterial2 = otherItem.GetComponent(); if (geneticMaterial1 != null && geneticMaterial2 != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index 43cba9600..528dee25a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -344,22 +344,27 @@ namespace Barotrauma.Items.Components var tempUser = user; for (int i = 0; i < (int)fabricationValueItem.Value; i++) { + float outCondition = fabricatedItem.OutCondition; if (i < amountFittingContainer) { - Entity.Spawner.AddToSpawnQueue(fabricatedItem.TargetItem, outputContainer.Inventory, fabricatedItem.TargetItem.Health * fabricatedItem.OutCondition, - onSpawned: (Item spawnedItem) => - { + Entity.Spawner.AddToSpawnQueue(fabricatedItem.TargetItem, outputContainer.Inventory, fabricatedItem.TargetItem.Health * outCondition, + onSpawned: (Item spawnedItem) => + { onItemSpawned(spawnedItem, tempUser); spawnedItem.Quality = quality; + //reset the condition in case the max condition is higher than the prefab's due to e.g. quality modifiers + spawnedItem.Condition = spawnedItem.MaxCondition * outCondition; }); } else { - Entity.Spawner.AddToSpawnQueue(fabricatedItem.TargetItem, item.Position, item.Submarine, fabricatedItem.TargetItem.Health * fabricatedItem.OutCondition, - onSpawned: (Item spawnedItem) => - { + Entity.Spawner.AddToSpawnQueue(fabricatedItem.TargetItem, item.Position, item.Submarine, fabricatedItem.TargetItem.Health * outCondition, + onSpawned: (Item spawnedItem) => + { onItemSpawned(spawnedItem, tempUser); spawnedItem.Quality = quality; + //reset the condition in case the max condition is higher than the prefab's due to e.g. quality modifiers + spawnedItem.Condition = spawnedItem.MaxCondition * outCondition; }); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index 07f99e1ea..0cfd93de8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -137,6 +137,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(false, false, description: "Can the item stick even to deflective targets.")] + public bool StickToDeflective + { + get; + set; + } + [Serialize(false, false, description: "Hitscan projectiles cast a ray forwards and immediately hit whatever the ray hits. "+ "It is recommended to use hitscans for very fast-moving projectiles such as bullets, because using extremely fast launch velocities may cause physics glitches.")] public bool Hitscan @@ -231,7 +238,10 @@ namespace Barotrauma.Items.Components { Item.body.ResetDynamics(); Item.SetTransform(simPosition, rotation); - Attack.DamageMultiplier = damageMultiplier; + if (Attack != null) + { + Attack.DamageMultiplier = damageMultiplier; + } // Set user for hitscan projectiles to work properly. User = user; // Need to set null for non-characterusable items. @@ -356,6 +366,7 @@ namespace Barotrauma.Items.Components { float rotation = item.body.Rotation; Vector2 simPositon = item.SimPosition; + Vector2 rayStartWorld = item.WorldPosition; item.Drop(null); item.body.Enabled = true; @@ -367,7 +378,6 @@ namespace Barotrauma.Items.Components Vector2 rayStart = simPositon; Vector2 rayEnd = rayStart + dir * 500.0f; - Vector2 rayStartWorld = item.WorldPosition; float worldDist = 1000.0f; #if CLIENT worldDist = Screen.Selected?.Cam?.WorldView.Width ?? GameMain.GraphicsWidth; @@ -579,7 +589,8 @@ namespace Barotrauma.Items.Components if (!removePending) { - ApplyStatusEffects(ActionType.OnActive, deltaTime, null); + Entity useTarget = lastTarget?.Body.UserData is Limb limb ? limb.character : lastTarget?.Body.UserData as Entity; + ApplyStatusEffects(ActionType.OnActive, deltaTime, useTarget: useTarget, user: _user); } if (item.body != null && item.body.FarseerBody.IsBullet) @@ -624,7 +635,6 @@ namespace Barotrauma.Items.Components return false; } - private bool OnProjectileCollision(Fixture f1, Fixture target, Contact contact) { if (User != null && User.Removed) { User = null; return false; } @@ -695,7 +705,8 @@ namespace Barotrauma.Items.Components } } - readonly List targets = new List(); + private readonly List targets = new List(); + private Fixture lastTarget; private bool HandleProjectileCollision(Fixture target, Vector2 collisionNormal, Vector2 velocity) { @@ -706,6 +717,7 @@ namespace Barotrauma.Items.Components { return false; } + lastTarget = target; float projectileNewSpeed = 0.5f; float projectileDeflectedNewSpeed = 0.1f; @@ -836,14 +848,16 @@ namespace Barotrauma.Items.Components } if (attackResult.AppliedDamageModifiers != null && - attackResult.AppliedDamageModifiers.Any(dm => dm.DeflectProjectiles)) + (attackResult.AppliedDamageModifiers.Any(dm => dm.DeflectProjectiles) && !StickToDeflective)) { item.body.LinearVelocity *= projectileDeflectedNewSpeed; } - else if (Vector2.Dot(velocity, collisionNormal) < 0.0f && hits.Count() >= MaxTargetsToHit && + else if ( // When hitting characters the collision normal seems to sometimes point into wrong direction, resulting in a failed attempt to stick + //Vector2.Dot(Vector2.Normalize(velocity), collisionNormal) < 0.0f && + hits.Count() >= MaxTargetsToHit && target.Body.Mass > item.body.Mass * 0.5f && (DoesStick || - (StickToCharacters && target.Body.UserData is Limb) || + (StickToCharacters && (target.Body.UserData is Limb || target.Body.UserData is Character)) || (StickToStructures && target.Body.UserData is Structure) || (StickToItems && target.Body.UserData is Item))) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs index 3bfab45f0..aaf95429a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Quality.cs @@ -35,7 +35,7 @@ namespace Barotrauma.Items.Components private int qualityLevel; - [Serialize(0, false)] + [Serialize(0, true)] public int QualityLevel { get { return qualityLevel; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs index 875942fca..b0885d400 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs @@ -406,7 +406,15 @@ namespace Barotrauma.Items.Components fixDuration /= 1 + CurrentFixer.GetStatValue(StatTypes.RepairSpeed) + currentRepairItem?.Prefab.AddedRepairSpeedMultiplier ?? 0f; fixDuration /= 1 + item.GetQualityModifier(Quality.StatType.RepairSpeed); - item.MaxRepairConditionMultiplier = 1 + CurrentFixer.GetStatValue(StatTypes.MaxRepairConditionMultiplier); + // kind of rough to keep this in update, but seems most robust + if (requiredSkills.Any(s => s != null && s.Identifier.Equals("mechanical", StringComparison.OrdinalIgnoreCase))) + { + item.MaxRepairConditionMultiplier = 1 + CurrentFixer.GetStatValue(StatTypes.MaxRepairConditionMultiplierMechanical); + } + if (requiredSkills.Any(s => s != null && s.Identifier.Equals("electrical", StringComparison.OrdinalIgnoreCase))) + { + item.MaxRepairConditionMultiplier = 1 + CurrentFixer.GetStatValue(StatTypes.MaxRepairConditionMultiplierElectrical); + } if (currentFixerAction == FixActions.Repair) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs index 1f923cc23..de4ae2389 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs @@ -3,7 +3,6 @@ using FarseerPhysics; using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using System; -using System.Linq; using System.Xml.Linq; namespace Barotrauma.Items.Components @@ -54,6 +53,27 @@ namespace Barotrauma.Items.Components set; } + [Serialize(true, false, description: "Should the rope snap when the character drops the aim?")] + public bool SnapWhenNotAimed + { + get; + set; + } + + [Serialize(30.0f, false, description: "How much mass is required for the target to pull the source towards it. Static and kinematic targets are always treated heavy enough.")] + public float TargetMinMass + { + get; + set; + } + + [Serialize(false, false)] + public bool LerpForces + { + get; + set; + } + private bool snapped; public bool Snapped { @@ -85,6 +105,7 @@ namespace Barotrauma.Items.Components partial void InitProjSpecific(XElement element); + public void Snap() => Snapped = true; public void Attach(ISpatialEntity source, Item target) { @@ -118,14 +139,15 @@ namespace Barotrauma.Items.Components Vector2 diff = target.WorldPosition - source.WorldPosition; if (diff.LengthSquared() > MaxLength * MaxLength) { - Snapped = true; + Snap(); return; } #if CLIENT item.ResetCachedVisibleSize(); #endif - + var projectile = target.GetComponent(); + if (projectile == null) { return; } if (SnapOnCollision) { raycastTimer += deltaTime; @@ -135,28 +157,24 @@ namespace Barotrauma.Items.Components collisionCategory: Physics.CollisionLevel | Physics.CollisionWall, customPredicate: (Fixture f) => { - var projectile = target?.GetComponent(); - if (projectile != null) + foreach (Body body in projectile.Hits) { - foreach (Body body in projectile.Hits) + Submarine alreadyHitSub = null; + if (body.UserData is Structure hitStructure) { - Submarine alreadyHitSub = null; - if (body.UserData is Structure hitStructure) - { - alreadyHitSub = hitStructure.Submarine; - } - else if (body.UserData is Submarine hitSub) - { - alreadyHitSub = hitSub; - } - if (alreadyHitSub != null) - { - if (f.Body?.UserData is MapEntity me && me.Submarine == alreadyHitSub) { return false; } - if (f.Body?.UserData as Submarine == alreadyHitSub) { return false; } - } + alreadyHitSub = hitStructure.Submarine; + } + else if (body.UserData is Submarine hitSub) + { + alreadyHitSub = hitSub; + } + if (alreadyHitSub != null) + { + if (f.Body?.UserData is MapEntity me && me.Submarine == alreadyHitSub) { return false; } + if (f.Body?.UserData as Submarine == alreadyHitSub) { return false; } } } - Submarine targetSub = target?.GetComponent()?.StickTarget?.UserData as Submarine ?? target.Submarine; + Submarine targetSub = projectile.StickTarget?.UserData as Submarine ?? target.Submarine; if (f.Body?.UserData is MapEntity mapEntity && mapEntity.Submarine != null) { @@ -175,7 +193,7 @@ namespace Barotrauma.Items.Components return true; }) != null) { - Snapped = true; + Snap(); return; } raycastTimer = 0.0f; @@ -183,27 +201,107 @@ namespace Barotrauma.Items.Components } Vector2 forceDir = diff; - if (forceDir.LengthSquared() > 0.01f) + float distance = diff.Length(); + if (distance > 0.001f) { forceDir = Vector2.Normalize(forceDir); } if (Math.Abs(ProjectilePullForce) > 0.001f) { - var projectile = target.GetComponent(); - projectile?.Item?.body?.ApplyForce(-forceDir * ProjectilePullForce); + projectile.Item?.body?.ApplyForce(-forceDir * ProjectilePullForce); } - if (Math.Abs(SourcePullForce) > 0.001f) + if (projectile.StickTarget != null) { - var sourceBody = GetBodyToPull(source); - sourceBody?.ApplyForce(forceDir * SourcePullForce); - } - - if (Math.Abs(TargetPullForce) > 0.001f) - { - var targetBody = GetBodyToPull(target); - targetBody?.ApplyForce(-forceDir * TargetPullForce); + float targetMass = float.MaxValue; + Character targetCharacter = null; + if (projectile.StickTarget.UserData is Limb targetLimb) + { + targetCharacter = targetLimb.character; + targetMass = targetLimb.ragdoll.Mass; + } + else if (projectile.StickTarget.UserData is Character character) + { + targetCharacter = character; + targetMass = character.Mass; + } + else if (projectile.StickTarget.UserData is Item item) + { + targetMass = projectile.StickTarget.Mass; + } + if (projectile.StickTarget.BodyType != BodyType.Dynamic) + { + targetMass = float.MaxValue; + } + var user = item.GetComponent()?.User; + if (targetMass > TargetMinMass) + { + if (Math.Abs(SourcePullForce) > 0.001f) + { + var sourceBody = GetBodyToPull(source); + if (sourceBody != null) + { + var targetBody = GetBodyToPull(target); + if (targetBody != null && !(targetBody.UserData is Character)) + { + sourceBody.ApplyForce(targetBody.LinearVelocity * sourceBody.Mass); + } + float forceMultiplier = 1; + if (user != null) + { + user.AnimController.Hang(); + if (user.InWater) + { + if (user.IsRagdolled) + { + forceMultiplier = 0; + } + } + else + { + forceMultiplier = user.IsRagdolled ? 0.1f : 0.4f; + // Prevents too easy smashing to the walls + forceDir.X /= 4; + // Prevents rubberbanding up and down + if (forceDir.Y < 0) + { + forceDir.Y = 0; + } + } + if (targetCharacter != null) + { + var myCollider = user.AnimController.Collider; + var targetCollider = targetCharacter.AnimController.Collider; + if (myCollider.LinearVelocity != Vector2.Zero && targetCollider.LinearVelocity != Vector2.Zero) + { + if (Vector2.Dot(Vector2.Normalize(myCollider.LinearVelocity), Vector2.Normalize(targetCollider.LinearVelocity)) < 0) + { + myCollider.ApplyForce(targetCollider.LinearVelocity * targetCollider.Mass); + } + } + } + } + float force = LerpForces ? MathHelper.Lerp(0, SourcePullForce, MathUtils.InverseLerp(0, MaxLength / 2, distance)) * forceMultiplier : SourcePullForce * forceMultiplier; + sourceBody.ApplyForce(forceDir * force); + } + } + } + if (Math.Abs(TargetPullForce) > 0.001f) + { + var targetBody = GetBodyToPull(target); + if (user != null && targetCharacter != null && !user.AnimController.InWater) + { + // Prevents rubberbanding horizontally when dragging a corpse. + if ((forceDir.X < 0) != (user.AnimController.Dir < 0)) + { + forceDir.X = Math.Clamp(forceDir.X, -0.1f, 0.1f); + } + } + float force = LerpForces ? MathHelper.Lerp(0, TargetPullForce, MathUtils.InverseLerp(0, MaxLength / 3, distance)) : TargetPullForce; + targetBody?.ApplyForce(-forceDir * force); + targetCharacter?.AnimController.Collider.ApplyForce(-forceDir * force * 3); + } } } @@ -231,19 +329,23 @@ namespace Barotrauma.Items.Components return ownerCharacter.AnimController.Collider; } var projectile = targetItem.GetComponent(); - if (projectile != null) + if (projectile != null && projectile.StickTarget != null) { - if (projectile.StickTarget?.UserData is Structure structure) + if (projectile.StickTarget.UserData is Structure structure) { return structure.Submarine?.PhysicsBody; } - else if (projectile.StickTarget?.UserData is Submarine sub) + else if (projectile.StickTarget.UserData is Submarine sub) { - return sub?.PhysicsBody; + return sub.PhysicsBody; } - else if (projectile.StickTarget?.UserData is Character character) + else if (projectile.StickTarget.UserData is Item item) { - return character.AnimController.Collider; + return item.body; + } + else if (projectile.StickTarget.UserData is Limb limb) + { + return limb.body; } return null; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Scanner.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Scanner.cs new file mode 100644 index 000000000..9c4998801 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Scanner.cs @@ -0,0 +1,90 @@ +using System; +using System.Xml.Linq; + +namespace Barotrauma.Items.Components +{ + partial class Scanner : ItemComponent + { + [Serialize(1.0f, false, description: "How long it takes for the scan to be completed.")] + public float ScanDuration { get; set; } + [Serialize(0.0f, false, description: "How far along the scan is. When the timer goes above ScanDuration, the scan is completed.")] + public float ScanTimer + { + get + { + return scanTimer; + } + set + { + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } + if (Holdable == null) { return; } + bool wasScanCompletedPreviously = IsScanCompleted; + scanTimer = Math.Max(0.0f, value); + if (!wasScanCompletedPreviously && IsScanCompleted) + { + OnScanCompleted?.Invoke(this); + } +#if SERVER + if (wasScanCompletedPreviously != IsScanCompleted || Math.Abs(LastSentScanTimer - scanTimer) > 0.1f) + { + item.CreateServerEvent(this); + LastSentScanTimer = scanTimer; + } +#endif + } + } + [Serialize(1.0f, false, description: "How far the scanner can be from the target for the scan to be successful.")] + public float ScanRadius { get; set; } + [Serialize(true, false, description: "Should the progress bar always be displayed when the item has been attached.")] + public bool AlwaysDisplayProgressBar { get; set; } + + private Holdable Holdable { get; set; } + /// + /// Should the progress bar be displayed. Use when AlwaysDisplayProgressBar is set to false. + /// + public bool DisplayProgressBar { get; set; } = false; + private bool IsScanCompleted => scanTimer >= ScanDuration; + + private float scanTimer; + + public Action OnScanStarted, OnScanCompleted; + + public Scanner(Item item, XElement element) : base(item, element) + { + IsActive = true; + } + + public override void Update(float deltaTime, Camera cam) + { + if (Holdable != null && Holdable.Attachable && Holdable.Attached) + { + if (ScanTimer <= 0.0f) + { + OnScanStarted?.Invoke(this); + } + ScanTimer += deltaTime; + item.AiTarget?.IncreaseSoundRange(deltaTime, speed: 2.0f); + ApplyStatusEffects(ActionType.OnActive, deltaTime); + } + else + { + ScanTimer = 0.0f; + DisplayProgressBar = false; + } + UpdateProjSpecific(); + } + + partial void UpdateProjSpecific(); + + public override void OnItemLoaded() + { + base.OnItemLoaded(); + Holdable = item.GetComponent(); + if (Holdable == null || !Holdable.Attachable) + { + DebugConsole.ThrowError("Error in initializing a Scanner component: an attachable Holdable component is required on the same item and none was found"); + IsActive = false; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ButtonTerminal.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ButtonTerminal.cs new file mode 100644 index 000000000..88883ccca --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ButtonTerminal.cs @@ -0,0 +1,119 @@ +using Barotrauma.Extensions; +using Barotrauma.Networking; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Items.Components +{ + partial class ButtonTerminal : ItemComponent + { + [Editable, Serialize(new string[0], true, description: "Signals sent when the corresponding buttons are pressed.", alwaysUseInstanceValues: true)] + public string[] Signals { get; set; } + [Editable, Serialize("", true, description: "Identifiers or tags of items that, when contained, allow the terminal buttons to be used. Multiple ones should be separated by commas.", alwaysUseInstanceValues: true)] + public string ActivatingItems { get; set; } + + private int RequiredSignalCount { get; set; } + private ItemContainer Container { get; set; } + private HashSet ActivatingItemPrefabs { get; set; } = new HashSet(); + + + private bool AllowUsingButtons => ActivatingItemPrefabs.None() || Container.Inventory.AllItems.Any(i => i != null && ActivatingItemPrefabs.Any(p => p == i.Prefab)); + + public ButtonTerminal(Item item, XElement element) : base(item, element) + { + IsActive = true; + RequiredSignalCount = element.GetChildElements("TerminalButton").Count(c => c.GetAttribute("style") != null); + if (RequiredSignalCount < 1) + { + DebugConsole.ThrowError($"Error in item \"{item.Name}\": no TerminalButton elements defined for the ButtonTerminal component!"); + } + InitProjSpecific(element); + } + + partial void InitProjSpecific(XElement element); + + public override void OnItemLoaded() + { + base.OnItemLoaded(); + + if (Signals == null) + { + Signals = new string[RequiredSignalCount]; + for (int i = 0; i < RequiredSignalCount; i++) + { + Signals[i] = string.Empty; + } + } + else if (Signals.Length != RequiredSignalCount) + { + string[] newSignals = new string[RequiredSignalCount]; + if (Signals.Length < RequiredSignalCount) + { + Signals.CopyTo(newSignals, 0); + for (int i = Signals.Length; i < RequiredSignalCount; i++) + { + newSignals[i] = string.Empty; + } + } + else + { + for (int i = 0; i < RequiredSignalCount; i++) + { + newSignals[i] = Signals[i]; + } + } + Signals = newSignals; + } + + ActivatingItemPrefabs.Clear(); + if (!string.IsNullOrEmpty(ActivatingItems)) + { + foreach (var activatingItem in ActivatingItems.Split(',')) + { + if (MapEntityPrefab.Find(null, identifier: activatingItem, showErrorMessages: false) is ItemPrefab prefab) + { + ActivatingItemPrefabs.Add(prefab); + } + else + { + ItemPrefab.Prefabs.Where(p => p.Tags.Any(t => t.Equals(activatingItem, StringComparison.OrdinalIgnoreCase))) + .ForEach(p => ActivatingItemPrefabs.Add(p)); + } + } + if (ActivatingItemPrefabs.None()) + { + DebugConsole.ThrowError($"Error in item \"{item.Name}\": no activating item prefabs found with identifiers or tags \"{ActivatingItems}\""); + } + } + + var containers = item.GetComponents().ToList(); + if (containers.Count != 1) + { + DebugConsole.ThrowError($"Error in item \"{item.Name}\": the ButtonTerminal component requires exactly one ItemContainer component!"); + return; + } + Container = containers[0]; + + OnItemLoadedProjSpecific(); + } + + partial void OnItemLoadedProjSpecific(); + + private bool SendSignal(int signalIndex, bool isServerMessage = false) + { + if (!isServerMessage && !AllowUsingButtons) { return false; } + string signal = Signals[signalIndex]; + string connectionName = $"signal_out{signalIndex + 1}"; + item.SendSignal(signal, connectionName); + return true; + } + + private void Write(IWriteMessage msg, object[] extraData) + { + if (extraData == null || extraData.Length < 3) { return; } + msg.WriteRangedInteger((int)extraData[2], 0, Signals.Length - 1); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs index 09339aabc..90db70255 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs @@ -101,7 +101,7 @@ namespace Barotrauma.Items.Components foreach (XElement connectionElement in subElement.Elements()) { - string prefabConnectionName = element.GetAttributeString("name", null); + string prefabConnectionName = connectionElement.GetAttributeString("name", null); if (prefabConnectionName == Name) { displayNameTag = connectionElement.GetAttributeString("displayname", ""); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs new file mode 100644 index 000000000..197bdd2c4 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs @@ -0,0 +1,185 @@ +using FarseerPhysics; +using FarseerPhysics.Dynamics; +using FarseerPhysics.Dynamics.Contacts; +using Microsoft.Xna.Framework; +using System; +using System.Linq; +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Barotrauma.Items.Components +{ + class TriggerComponent : ItemComponent + { + [Editable, Serialize(0.0f, true, description: "The maximum amount of force applied to the triggering entitites.", alwaysUseInstanceValues: true)] + public float Force { get; set; } + + public PhysicsBody PhysicsBody { get; private set; } + private float Radius { get; set; } + private float RadiusInDisplayUnits { get; set; } + private bool TriggeredOnce { get; set; } + + public bool TriggerActive { get; private set; } + + private readonly LevelTrigger.TriggererType triggeredBy; + private readonly HashSet triggerers = new HashSet(); + private readonly bool triggerOnce; + private readonly List statusEffectTargets = new List(); + /// + /// Effects applied to entities inside the trigger + /// + private readonly List statusEffects = new List(); + /// + /// Attacks applied to entities inside the trigger + /// + private readonly List attacks = new List(); + + public TriggerComponent(Item item, XElement element) : base(item, element) + { + string triggeredByAttribute = element.GetAttributeString("triggeredby", "Character"); + if (!Enum.TryParse(triggeredByAttribute, out triggeredBy)) + { + DebugConsole.ThrowError($"Error in ForceComponent config: \"{triggeredByAttribute}\" is not a valid triggerer type."); + } + triggerOnce = element.GetAttributeBool("triggeronce", false); + string parentDebugName = $"TriggerComponent in {item.Name}"; + foreach (XElement subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "statuseffect": + LevelTrigger.LoadStatusEffect(statusEffects, subElement, parentDebugName); + break; + case "attack": + case "damage": + LevelTrigger.LoadAttack(subElement, parentDebugName, triggerOnce, attacks); + break; + } + } + IsActive = true; + } + + public override void OnItemLoaded() + { + base.OnItemLoaded(); + float radiusAttribute = originalElement.GetAttributeFloat("radius", 10.0f); + Radius = ConvertUnits.ToSimUnits(radiusAttribute * item.Scale); + PhysicsBody = new PhysicsBody(0.0f, 0.0f, Radius, 1.5f) + { + BodyType = BodyType.Static, + CollidesWith = LevelTrigger.GetCollisionCategories(triggeredBy), + CollisionCategories = Physics.CollisionWall, + UserData = item + }; + PhysicsBody.FarseerBody.SetIsSensor(true); + PhysicsBody.FarseerBody.OnCollision += OnCollision; + PhysicsBody.FarseerBody.OnSeparation += OnSeparation; + RadiusInDisplayUnits = ConvertUnits.ToDisplayUnits(PhysicsBody.radius); + } + + public override void OnMapLoaded() + { + base.OnMapLoaded(); + PhysicsBody.SetTransformIgnoreContacts(item.SimPosition, 0.0f); + PhysicsBody.Submarine = item.Submarine; + } + + private bool OnCollision(Fixture sender, Fixture other, Contact contact) + { + if (!(LevelTrigger.GetEntity(other) is Entity entity)) { return false; } + if (!LevelTrigger.IsTriggeredByEntity(entity, triggeredBy, mustBeOnSpecificSub: (true, item.Submarine))) { return false; } + triggerers.Add(entity); + return true; + } + + private void OnSeparation(Fixture sender, Fixture other, Contact contact) + { + if (!(LevelTrigger.GetEntity(other) is Entity entity)) + { + return; + } + if (entity is Character character && (!character.Enabled || character.Removed) && triggerers.Contains(entity)) + { + triggerers.Remove(entity); + return; + } + if (LevelTrigger.CheckContactsForOtherFixtures(PhysicsBody, other, entity)) + { + return; + } + triggerers.Remove(entity); + } + + public override void Update(float deltaTime, Camera cam) + { + triggerers.RemoveWhere(t => t.Removed); + LevelTrigger.RemoveDistantTriggerers(PhysicsBody, triggerers, item.WorldPosition); + + if (triggerOnce) + { + if (TriggeredOnce) { return; } + if (triggerers.Count > 0) + { + TriggeredOnce = true; + IsActive = false; + triggerers.Clear(); + } + } + + TriggerActive = triggerers.Any(); + + foreach (Entity triggerer in triggerers) + { + LevelTrigger.ApplyStatusEffects(statusEffects, item.WorldPosition, triggerer, deltaTime, statusEffectTargets); + + if (triggerer is IDamageable damageable) + { + LevelTrigger.ApplyAttacks(attacks, damageable, item.WorldPosition, deltaTime); + } + else if (triggerer is Submarine submarine) + { + LevelTrigger.ApplyAttacks(attacks, item.WorldPosition, deltaTime); + } + + if (Force < 0.01f) + { + // Just ignore very minimal forces + continue; + } + else if (triggerer is Character c) + { + ApplyForce(c.AnimController.Collider); + } + else if (triggerer is Submarine s) + { + ApplyForce(s.SubBody.Body); + } + else if (triggerer is Item i && i.body != null) + { + ApplyForce(i.body); + } + } + } + + private void ApplyForce(PhysicsBody body) + { + Vector2 diff = ConvertUnits.ToDisplayUnits(PhysicsBody.SimPosition - body.SimPosition); + if (diff.LengthSquared() < 0.0001f) { return; } + float distanceFactor = LevelTrigger.GetDistanceFactor(body, PhysicsBody, RadiusInDisplayUnits); + if (distanceFactor <= 0.0f) { return; } + Vector2 force = distanceFactor * Force * Vector2.Normalize(diff); + if (force.LengthSquared() < 0.01f) { return; } + body.ApplyForce(force); + } + + public override void Move(Vector2 amount) + { + base.Move(amount); + if (PhysicsBody != null) + { + PhysicsBody.SetTransform(PhysicsBody.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f); + PhysicsBody.Submarine = item.Submarine; + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs index 62995f7c4..00a144ba0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs @@ -413,19 +413,19 @@ namespace Barotrauma.Items.Components return i1.WearableComponent.AllowedSlots.Contains(InvSlotType.OuterClothes).CompareTo(i2.WearableComponent.AllowedSlots.Contains(InvSlotType.OuterClothes)); }); } - #if CLIENT equipLimb.UpdateWearableTypesToHide(); #endif } + character.OnWearablesChanged(); } public override void Drop(Character dropper) { + Character previousPicker = picker; Unequip(picker); - base.Drop(dropper); - + previousPicker?.OnWearablesChanged(); picker = null; IsActive = false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 119d7ddd5..2d59645bb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -11,6 +11,7 @@ using System.Linq; using System.Xml.Linq; using Barotrauma.Extensions; using Barotrauma.MapCreatures.Behavior; +using Barotrauma.Abilities; #if CLIENT using Microsoft.Xna.Framework.Graphics; @@ -36,7 +37,6 @@ namespace Barotrauma set { currentHull = value; - ParentRuin = currentHull?.ParentRuin; } } @@ -975,6 +975,9 @@ namespace Barotrauma if (Components.Any(ic => ic is Wire) && Components.All(ic => ic is Wire || ic is Holdable)) { isWire = true; } if (HasTag("logic")) { isLogic = true; } + + ApplyStatusEffects(ActionType.OnSpawn, 1.0f); + Components.ForEach(c => c.ApplyStatusEffects(ActionType.OnSpawn, 1.0f)); } partial void InitProjSpecific(); @@ -1603,7 +1606,10 @@ namespace Barotrauma } } - aiTarget?.Update(deltaTime); + if (aiTarget != null) + { + aiTarget.Update(deltaTime); + } if (!isActive) { return; } @@ -2351,6 +2357,8 @@ namespace Barotrauma } #endif + float applyOnSelfFraction = user?.GetStatValue(StatTypes.ApplyTreatmentsOnSelfFraction) ?? 0.0f; + bool remove = false; foreach (ItemComponent ic in components) { @@ -2363,7 +2371,19 @@ namespace Barotrauma ic.PlaySound(actionType, user); #endif ic.WasUsed = true; - ic.ApplyStatusEffects(actionType, 1.0f, character, targetLimb, user: user); + ic.ApplyStatusEffects(actionType, 1.0f, character, targetLimb, user: user, applyOnUserFraction: applyOnSelfFraction); + + if (applyOnSelfFraction > 0.0f) + { + //hacky af + ic.statusEffectLists.TryGetValue(actionType, out var effectList); + if (effectList != null) + { + effectList.ForEach(e => e.AfflictionMultiplier = applyOnSelfFraction); + ic.ApplyStatusEffects(actionType, 1.0f, user, targetLimb == null ? null : user.AnimController.GetLimb(targetLimb.type), user: user); + effectList.ForEach(e => e.AfflictionMultiplier = 1.0f); + } + } if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { @@ -2376,6 +2396,15 @@ namespace Barotrauma if (ic.DeleteOnUse) { remove = true; } } + if (user != null) + { + var abilityItem = new AbilityApplyTreatment(user, character, this); + user.CheckTalents(AbilityEffectType.OnApplyTreatment, abilityItem); + + } + + + if (remove) { Spawner?.AddToRemoveQueue(this); } } @@ -2549,6 +2578,14 @@ namespace Barotrauma { msg.Write((int)value); } + else if (value is string[] a) + { + msg.Write(a.Length); + for (int i = 0; i < a.Length; i++) + { + msg.Write(a[i] ?? ""); + } + } else { throw new NotImplementedException("Serializing item properties of the type \"" + value.GetType() + "\" not supported"); @@ -2656,6 +2693,19 @@ namespace Barotrauma logValue = XMLExtensions.RectToString(val); if (allowEditing) { property.TrySetValue(parentObject, val); } } + else if (type == typeof(string[])) + { + int arrayLength = msg.ReadInt32(); + string[] val = new string[arrayLength]; + for (int i = 0; i < arrayLength; i++) + { + val[i] = msg.ReadString(); + } + if (allowEditing) + { + property.TrySetValue(parentObject, val); + } + } else if (typeof(Enum).IsAssignableFrom(type)) { int intVal = msg.ReadInt32(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs index b09272cc1..47525f812 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs @@ -30,55 +30,44 @@ namespace Barotrauma private bool idFreed; - public virtual bool Removed - { - get; - private set; - } + public virtual bool Removed { get; private set; } - public bool IdFreed - { - get { return idFreed; } - } + public bool IdFreed => idFreed; public readonly ushort ID; - public virtual Vector2 SimPosition + public virtual Vector2 SimPosition => Vector2.Zero; + + public virtual Vector2 Position => Vector2.Zero; + + public virtual Vector2 WorldPosition => Submarine == null ? Position : Submarine.Position + Position; + + public virtual Vector2 DrawPosition => Submarine == null ? Position : Submarine.DrawPosition + Position; + + public Submarine Submarine { get; set; } + + public AITarget AiTarget => aiTarget; + + public bool InDetectable { - get { return Vector2.Zero; } - } - - public virtual Vector2 Position - { - get { return Vector2.Zero; } - } - - public virtual Vector2 WorldPosition - { - get { return Submarine == null ? Position : Submarine.Position + Position; } - } - - public virtual Vector2 DrawPosition - { - get { return Submarine == null ? Position : Submarine.DrawPosition + Position; } - } - - public Submarine Submarine - { - get; - set; - } - - public AITarget AiTarget - { - get { return aiTarget; } - } - - public double SpawnTime - { - get { return spawnTime; } + get + { + if (aiTarget != null) + { + return aiTarget.InDetectable; + } + return false; + } + set + { + if (aiTarget != null) + { + aiTarget.InDetectable = value; + } + } } + public double SpawnTime => spawnTime; private readonly double spawnTime; public Entity(Submarine submarine, ushort id) @@ -88,7 +77,7 @@ namespace Barotrauma if (id != NullEntityID && dictionary.ContainsKey(id)) { - throw new Exception($"ID {id} is taken by {dictionary[id].ToString()}"); + throw new Exception($"ID {id} is taken by {dictionary[id]}"); } //give a unique ID diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 5fd474745..350fb0795 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -325,6 +325,7 @@ namespace Barotrauma get { return LevelData.Seed; } } + public static float? ForcedDifficulty; public float Difficulty { @@ -527,6 +528,7 @@ namespace Barotrauma //create a tunnel from the lowest point in the main path to the abyss //to ensure there's a way to the abyss in all levels + Tunnel abyssTunnel = null; if (GenerationParams.CreateHoleToAbyss) { Point lowestPoint = mainPath.Nodes.First(); @@ -534,7 +536,7 @@ namespace Barotrauma { if (pathNode.Y < lowestPoint.Y) { lowestPoint = pathNode; } } - var abyssTunnel = new Tunnel( + abyssTunnel = new Tunnel( TunnelType.SidePath, new List() { lowestPoint, new Point(lowestPoint.X, 0) }, minWidth / 2, parentTunnel: mainPath); @@ -545,7 +547,7 @@ namespace Barotrauma for (int j = 0; j < sideTunnelCount; j++) { if (mainPath.Nodes.Count < 4) { break; } - var validTunnels = Tunnels.FindAll(t => t.Type != TunnelType.Cave && t != startPath && t != endPath && t != endHole); + var validTunnels = Tunnels.FindAll(t => t.Type != TunnelType.Cave && t != startPath && t != endPath && t != endHole && t != abyssTunnel); Tunnel tunnelToBranchOff = validTunnels[Rand.Int(validTunnels.Count, Rand.RandSync.Server)]; if (tunnelToBranchOff == null) { tunnelToBranchOff = mainPath; } @@ -558,7 +560,7 @@ namespace Barotrauma Tunnels.Add(new Tunnel(TunnelType.SidePath, sidePathNodes, pathWidth, parentTunnel: tunnelToBranchOff)); } - CalculateTunnelDistanceField(density: 1000); + CalculateTunnelDistanceField(null); GenerateSeaFloorPositions(); GenerateAbyssArea(); GenerateCaves(mainPath); @@ -690,7 +692,10 @@ namespace Barotrauma } } } - GenerateWaypoints(tunnel, parentTunnel: tunnel.ParentTunnel); + + bool connectToParentTunnel = tunnel.Type != TunnelType.Cave || tunnel.ParentTunnel.Type == TunnelType.Cave; + GenerateWaypoints(tunnel, parentTunnel: connectToParentTunnel ? tunnel.ParentTunnel : null); + EnlargePath(tunnel.Cells, tunnel.MinWidth); foreach (var pathCell in tunnel.Cells) { @@ -790,6 +795,15 @@ namespace Barotrauma cells.AddRange(abyssIsland.Cells); } + List ruinPositions = new List(); + for (int i = 0; i < GenerationParams.RuinCount; i++) + { + Point ruinSize = new Point(5000); + ruinPositions.Add(FindPosAwayFromMainPath((Math.Max(ruinSize.X, ruinSize.Y) + mainPath.MinWidth) * 1.2f, asCloseAsPossible: true, + limits: new Rectangle(new Point(ruinSize.X / 2, ruinSize.Y / 2), Size - ruinSize))); + CalculateTunnelDistanceField(ruinPositions); + } + //---------------------------------------------------------------------------------- // initialize the cells that are still left and insert them into the cell grid //---------------------------------------------------------------------------------- @@ -812,7 +826,9 @@ namespace Barotrauma //---------------------------------------------------------------------------------- // mirror if needed //---------------------------------------------------------------------------------- - + + int asdfasdf = Rand.Int(int.MaxValue, Rand.RandSync.Server); + if (mirror) { HashSet mirroredEdges = new HashSet(); @@ -850,6 +866,11 @@ namespace Barotrauma island.Area = new Rectangle(borders.Width - island.Area.Right, island.Area.Y, island.Area.Width, island.Area.Height); } + for (int i = 0; i < ruinPositions.Count; i++) + { + ruinPositions[i] = new Point(borders.Width - ruinPositions[i].X, ruinPositions[i].Y); + } + foreach (Cave cave in Caves) { cave.Area = new Rectangle(borders.Width - cave.Area.Right, cave.Area.Y, cave.Area.Width, cave.Area.Height); @@ -895,7 +916,7 @@ namespace Barotrauma startExitPosition.X = borders.Width - startExitPosition.X; endExitPosition.X = borders.Width - endExitPosition.X; - CalculateTunnelDistanceField(density: 1000); + CalculateTunnelDistanceField(ruinPositions); } foreach (VoronoiCell cell in cells) @@ -912,8 +933,23 @@ namespace Barotrauma foreach (Cave cave in Caves) { if (cave.Area.Y > 0) - { - CreatePathToClosestTunnel(cave.StartPos); + { + List cavePathCells = CreatePathToClosestTunnel(cave.StartPos); + + var mainTunnel = cave.Tunnels.Find(t => t.ParentTunnel.Type != TunnelType.Cave); + + WayPoint prevWp = mainTunnel.WayPoints.First(); + if (prevWp != null) + { + for (int i = 0; i < cavePathCells.Count; i++) + { + var newWaypoint = new WayPoint(cavePathCells[i].Center, SpawnType.Path, submarine: null); + ConnectWaypoints(prevWp, newWaypoint, 500.0f); + prevWp = newWaypoint; + } + var closestPathPoint = FindClosestWayPoint(prevWp.WorldPosition, mainTunnel.ParentTunnel.WayPoints); + ConnectWaypoints(prevWp, closestPathPoint, 500.0f); + } } List caveCells = new List(); @@ -939,9 +975,10 @@ namespace Barotrauma //---------------------------------------------------------------------------------- Ruins = new List(); - for (int i = 0; i < GenerationParams.RuinCount; i++) + for (int i = 0; i < ruinPositions.Count; i++) { - GenerateRuin(mainPath, mirror); + Rand.SetSyncedSeed(ToolBox.StringToInt(Seed) + i); + GenerateRuin(ruinPositions[i], mirror); } EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); @@ -1003,7 +1040,6 @@ namespace Barotrauma } } - #if CLIENT List<(List cells, Cave parentCave)> cellBatches = new List<(List, Cave)> { @@ -1082,7 +1118,6 @@ namespace Barotrauma } #endif - EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); //---------------------------------------------------------------------------------- @@ -1100,6 +1135,11 @@ namespace Barotrauma // connect side paths and cave branches to their parents //---------------------------------------------------------------------------------- + foreach (Ruin ruin in Ruins) + { + GenerateRuinWayPoints(ruin); + } + foreach (Tunnel tunnel in Tunnels) { if (tunnel.ParentTunnel == null) { continue; } @@ -1342,18 +1382,7 @@ namespace Barotrauma 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; } - - wayPoints[n].linkedTo.Add(newWaypoint); - newWaypoint.linkedTo.Add(wayPoints[n]); - - break; + wayPoints[wayPoints.Count - 2].ConnectTo(newWaypoint); } } @@ -1362,19 +1391,17 @@ namespace Barotrauma //connect to the tunnel we're branching off from if (parentTunnel != null) { - var parentStart = FindClosestWayPoint(wayPoints.First(), parentTunnel); + var parentStart = FindClosestWayPoint(wayPoints.First().WorldPosition, parentTunnel); if (parentStart != null) { - wayPoints.First().linkedTo.Add(parentStart); - parentStart.linkedTo.Add(wayPoints.First()); + wayPoints.First().ConnectTo(parentStart); } if (tunnel.Type != TunnelType.Cave || tunnel.ParentTunnel.Type == TunnelType.Cave) { - var parentEnd = FindClosestWayPoint(wayPoints.Last(), parentTunnel); + var parentEnd = FindClosestWayPoint(wayPoints.Last().WorldPosition, parentTunnel); if (parentEnd != null) { - wayPoints.Last().linkedTo.Add(parentEnd); - parentEnd.linkedTo.Add(wayPoints.Last()); + wayPoints.Last().ConnectTo(parentEnd); } } } @@ -1384,45 +1411,58 @@ namespace Barotrauma { foreach (WayPoint wayPoint in tunnel.WayPoints) { - var closestWaypoint = FindClosestWayPoint(wayPoint, parentTunnel); + var closestWaypoint = FindClosestWayPoint(wayPoint.WorldPosition, parentTunnel); if (closestWaypoint == null) { continue; } if (Submarine.PickBody( ConvertUnits.ToSimUnits(wayPoint.WorldPosition), ConvertUnits.ToSimUnits(closestWaypoint.WorldPosition), collisionCategory: Physics.CollisionLevel | Physics.CollisionWall) == null) { - Vector2 diff = closestWaypoint.WorldPosition - wayPoint.WorldPosition; - float dist = diff.Length(); float step = ConvertUnits.ToDisplayUnits(Steering.AutopilotMinDistToPathNode) * 0.8f; - - WayPoint prevWaypoint = wayPoint; - for (float x = step; x < dist - step; x += step) - { - var newWaypoint = new WayPoint(wayPoint.WorldPosition + (diff / dist * x), SpawnType.Path, submarine: null) - { - Tunnel = tunnel - }; - prevWaypoint.linkedTo.Add(newWaypoint); - newWaypoint.linkedTo.Add(prevWaypoint); - prevWaypoint = newWaypoint; - } - prevWaypoint.linkedTo.Add(closestWaypoint); - closestWaypoint.linkedTo.Add(prevWaypoint); + ConnectWaypoints(wayPoint, closestWaypoint, step).ForEach(wp => wp.Tunnel = tunnel); } } } - private static WayPoint FindClosestWayPoint(WayPoint wayPoint, Tunnel otherTunnel) + private List ConnectWaypoints(WayPoint wp1, WayPoint wp2, float interval) + { + List newWaypoints = new List(); + + Vector2 diff = wp2.WorldPosition - wp1.WorldPosition; + float dist = diff.Length(); + + WayPoint prevWaypoint = wp1; + for (float x = interval; x < dist - interval; x += interval) + { + var newWaypoint = new WayPoint(wp1.WorldPosition + (diff / dist * x), SpawnType.Path, submarine: null); + prevWaypoint.ConnectTo(newWaypoint); + prevWaypoint = newWaypoint; + newWaypoints.Add(newWaypoint); + } + prevWaypoint.ConnectTo(wp2); + + return newWaypoints; + } + + private static WayPoint FindClosestWayPoint(Vector2 worldPosition, Tunnel otherTunnel) + { + return FindClosestWayPoint(worldPosition, otherTunnel.WayPoints); + } + + private static WayPoint FindClosestWayPoint(Vector2 worldPosition, IEnumerable waypoints, Func filter = null) { float closestDist = float.PositiveInfinity; WayPoint closestWayPoint = null; - foreach (WayPoint otherWayPoint in otherTunnel.WayPoints) + foreach (WayPoint otherWayPoint in waypoints) { - float dist = Vector2.DistanceSquared(otherWayPoint.WorldPosition, wayPoint.WorldPosition); + float dist = Vector2.DistanceSquared(otherWayPoint.WorldPosition, worldPosition); if (dist < closestDist) { + if (filter != null) + { + if (!filter(otherWayPoint)) { continue; } + } closestDist = dist; closestWayPoint = otherWayPoint; - } } return closestWayPoint; @@ -1705,7 +1745,7 @@ namespace Barotrauma GenerateCave(caveParams, parentTunnel, cavePos, caveSize); - CalculateTunnelDistanceField(density: 1000); + CalculateTunnelDistanceField(null); } } @@ -1787,84 +1827,145 @@ namespace Barotrauma } } - private void GenerateRuin(Tunnel mainPath, bool mirror) + private void GenerateRuin(Point ruinPos, bool mirror) { var ruinGenerationParams = RuinGenerationParams.GetRandom(); - Point ruinSize = new Point( - 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; - - Point ruinPos = FindPosAwayFromMainPath((ruinRadius + mainPath.MinWidth) * 1.2f, asCloseAsPossible: true, - limits: new Rectangle(new Point(ruinSize.X / 2, ruinSize.Y / 2), Size - ruinSize)); - - VoronoiCell closestPathCell = null; - double closestDist = 0.0f; - foreach (VoronoiCell pathCell in mainPath.Cells) + LocationType locationType = StartLocation?.Type; + if (locationType == null) { - double dist = MathUtils.DistanceSquared(pathCell.Site.Coord.X, pathCell.Site.Coord.Y, ruinPos.X, ruinPos.Y); - if (closestPathCell == null || dist < closestDist) + locationType = LocationType.List.GetRandom(Rand.RandSync.Server); + if (ruinGenerationParams.AllowedLocationTypes.Any()) { - closestPathCell = pathCell; - closestDist = dist; + locationType = LocationType.List.Where(lt => + ruinGenerationParams.AllowedLocationTypes.Any(allowedType => + allowedType.Equals("any", StringComparison.OrdinalIgnoreCase) || lt.Identifier.Equals(allowedType, StringComparison.OrdinalIgnoreCase))).GetRandom(); } } - - var ruin = new Ruin(closestPathCell, cells, ruinGenerationParams, new Rectangle(ruinPos - new Point(ruinSize.X / 2, ruinSize.Y / 2), ruinSize), mirror); + + var ruin = new Ruin(this, ruinGenerationParams, locationType, ruinPos, mirror); Ruins.Add(ruin); - - ruin.RuinShapes.Sort((shape1, shape2) => shape2.DistanceFromEntrance.CompareTo(shape1.DistanceFromEntrance)); - // TODO: autogenerate waypoints inside the ruins and connect them to the main path in multiple places. - // We need the waypoints for the AI navigation and we could use them for spawning the creatures too. - int waypointCount = 0; - foreach (WayPoint wp in WayPoint.WayPointList) + var tooClose = GetTooCloseCells(ruinPos.ToVector2(), Math.Max(ruin.Area.Width, ruin.Area.Height) * 4); + + foreach (VoronoiCell cell in tooClose) { - if (wp.SpawnType != SpawnType.Enemy || wp.Submarine != null) { continue; } - if (ruin.RuinShapes.Any(rs => rs.Rect.Contains(wp.WorldPosition))) + if (cell.CellType == CellType.Empty) { continue; } + if (ExtraWalls.Any(w => w.Cells.Contains(cell))) { continue; } + foreach (GraphEdge e in cell.Edges) { - PositionsOfInterest.Add(new InterestingPosition(new Point((int)wp.WorldPosition.X, (int)wp.WorldPosition.Y), PositionType.Ruin, ruin: ruin)); - waypointCount++; - } - } - - //not enough waypoints inside ruins -> create some spawn positions manually - for (int i = 0; i < 4 - waypointCount && i < ruin.RuinShapes.Count; i++) - { - PositionsOfInterest.Add(new InterestingPosition(ruin.RuinShapes[i].Rect.Center, PositionType.Ruin, ruin: ruin)); - } - - foreach (RuinShape ruinShape in ruin.RuinShapes) - { - var tooClose = GetTooCloseCells(ruinShape.Rect.Center.ToVector2(), Math.Max(ruinShape.Rect.Width, ruinShape.Rect.Height) * 4); - - foreach (VoronoiCell cell in tooClose) - { - if (cell.CellType == CellType.Empty) { continue; } - if (ExtraWalls.Any(w => w.Cells.Contains(cell))) { continue; } - foreach (GraphEdge e in cell.Edges) + if (ruin.Area.Contains(e.Point1) || ruin.Area.Contains(e.Point2) || + MathUtils.GetLineRectangleIntersection(e.Point1, e.Point2, ruin.Area, out _)) { - Rectangle rect = ruinShape.Rect; - rect.Y += rect.Height; - if (ruinShape.Rect.Contains(e.Point1) || ruinShape.Rect.Contains(e.Point2) || - MathUtils.GetLineRectangleIntersection(e.Point1, e.Point2, rect, out _)) + cell.CellType = CellType.Removed; + for (int x = 0; x < cellGrid.GetLength(0); x++) { - cell.CellType = CellType.Removed; - for (int x = 0; x < cellGrid.GetLength(0); x++) + for (int y = 0; y < cellGrid.GetLength(1); y++) { - for (int y = 0; y < cellGrid.GetLength(1); y++) - { - cellGrid[x, y].Remove(cell); - } + cellGrid[x, y].Remove(cell); } - cells.Remove(cell); - break; } + cells.Remove(cell); + break; } } } - CreatePathToClosestTunnel(ruinPos); + ruin.PathCells = CreatePathToClosestTunnel(ruin.Area.Center); + } + + private void GenerateRuinWayPoints(Ruin ruin) + { + var tooClose = GetTooCloseCells(ruin.Area.Center.ToVector2(), Math.Max(ruin.Area.Width, ruin.Area.Height) * 6); + + List wayPoints = new List(); + float outSideWaypointInterval = 500.0f; + WayPoint[,] cornerWaypoint = new WayPoint[2, 2]; + Rectangle waypointArea = ruin.Area; + waypointArea.Inflate(100, 100); + + //generate waypoints around the ruin + for (int i = 0; i < 2; i++) + { + for (float x = waypointArea.X + outSideWaypointInterval; x < waypointArea.Right - outSideWaypointInterval; x += outSideWaypointInterval) + { + var wayPoint = new WayPoint(new Vector2(x, waypointArea.Y + waypointArea.Height * i), SpawnType.Path, null); + wayPoints.Add(wayPoint); + if (x == waypointArea.X + outSideWaypointInterval) + { + cornerWaypoint[i, 0] = wayPoint; + } + else + { + wayPoint.ConnectTo(wayPoints[wayPoints.Count - 2]); + } + } + cornerWaypoint[i, 1] = wayPoints[wayPoints.Count - 1]; + } + + for (int i = 0; i < 2; i++) + { + WayPoint wayPoint = null; + for (float y = waypointArea.Y; y < waypointArea.Y + waypointArea.Height; y += outSideWaypointInterval) + { + wayPoint = new WayPoint(new Vector2(waypointArea.X + waypointArea.Width * i, y), SpawnType.Path, null); + wayPoints.Add(wayPoint); + if (y == waypointArea.Y) + { + wayPoint.ConnectTo(cornerWaypoint[0, i]); + } + else + { + wayPoint.ConnectTo(wayPoints[wayPoints.Count - 2]); + } + } + wayPoint.ConnectTo(cornerWaypoint[1, i]); + } + + //remove waypoints that are inside walls + for (int i = wayPoints.Count - 1; i >= 0; i--) + { + WayPoint wp = wayPoints[i]; + var overlappingCell = tooClose.Find(c => c.CellType != CellType.Removed && c.IsPointInside(wp.WorldPosition)); + if (overlappingCell == null) { continue; } + if (wp.linkedTo.Count > 1) + { + WayPoint linked1 = wp.linkedTo[0] as WayPoint; + WayPoint linked2 = wp.linkedTo[1] as WayPoint; + linked1.ConnectTo(linked2); + } + wp.Remove(); + wayPoints.RemoveAt(i); + } + + //connect ruin entrances to the outside waypoints + foreach (Gap g in Gap.GapList) + { + if (g.Submarine != ruin.Submarine || g.IsRoomToRoom) { continue; } + var gapWaypoint = WayPoint.WayPointList.Find(wp => wp.ConnectedGap == g); + if (gapWaypoint == null) { continue; } + var closestWp = FindClosestWayPoint(gapWaypoint.WorldPosition, wayPoints); + if (closestWp == null) { continue; } + gapWaypoint.ConnectTo(closestWp); + } + + //create a waypoint path from the ruin to the closest tunnel + WayPoint prevWp = FindClosestWayPoint(ruin.PathCells.First().Center, wayPoints, (wp) => + { + return Submarine.PickBody( + ConvertUnits.ToSimUnits(wp.WorldPosition), + ConvertUnits.ToSimUnits(ruin.PathCells.First().Center), collisionCategory: Physics.CollisionLevel | Physics.CollisionWall) == null; + }); + if (prevWp != null) + { + for (int i = 0; i < ruin.PathCells.Count; i++) + { + var newWaypoint = new WayPoint(ruin.PathCells[i].Center, SpawnType.Path, submarine: null); + ConnectWaypoints(prevWp, newWaypoint, outSideWaypointInterval); + prevWp = newWaypoint; + } + var closestPathPoint = FindClosestWayPoint(prevWp.WorldPosition, Tunnels.SelectMany(t => t.WayPoints)); + ConnectWaypoints(prevWp, closestPathPoint, outSideWaypointInterval); + } } private Point FindPosAwayFromMainPath(double minDistance, bool asCloseAsPossible, Rectangle? limits = null) @@ -1890,8 +1991,9 @@ namespace Barotrauma } } - private void CalculateTunnelDistanceField(int density) + private void CalculateTunnelDistanceField(List ruinPositions) { + int density = 1000; distanceField = new List<(Point point, double distance)>(); if (Mirrored) @@ -1926,6 +2028,23 @@ namespace Barotrauma shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.LineSegmentToPointDistanceSquared(tunnel.Nodes[i - 1], tunnel.Nodes[i], point)); } } + if (ruinPositions != null) + { + int ruinSize = 10000; + foreach (Point ruinPos in ruinPositions) + { + double xDiff = Math.Abs(point.X - ruinPos.X); + double yDiff = Math.Abs(point.Y - ruinPos.Y); + if (xDiff < ruinSize || yDiff < ruinSize) + { + shortestDistSqr = 0.0f; + } + else + { + shortestDistSqr = Math.Min(xDiff * xDiff + yDiff * yDiff, shortestDistSqr); + } + } + } shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.DistanceSquared((double)point.X, (double)point.Y, (double)startPosition.X, (double)startPosition.Y)); shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.DistanceSquared((double)point.X, (double)point.Y, (double)startExitPosition.X, (double)borders.Bottom)); shortestDistSqr = Math.Min(shortestDistSqr, MathUtils.DistanceSquared((double)point.X, (double)point.Y, (double)endPosition.X, (double)endPosition.Y)); @@ -2953,7 +3072,7 @@ namespace Barotrauma return closestCell; } - private void CreatePathToClosestTunnel(Point pos) + private List CreatePathToClosestTunnel(Point pos) { VoronoiCell closestPathCell = null; double closestDist = 0.0f; @@ -2973,6 +3092,7 @@ namespace Barotrauma //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); + List pathCells = new List() { closestPathCell }; foreach (VoronoiCell cell in validCells) { foreach (GraphEdge e in cell.Edges) @@ -2987,6 +3107,7 @@ namespace Barotrauma cellGrid[x, y].Remove(cell); } } + pathCells.Add(cell); cells.Remove(cell); //go through the edges of this cell and find the ones that are next to a removed cell @@ -3019,12 +3140,19 @@ namespace Barotrauma } } } - - break; - } } + + pathCells.Sort((c1, c2) => { return Vector2.DistanceSquared(c1.Center, pos.ToVector2()).CompareTo(Vector2.DistanceSquared(c2.Center, pos.ToVector2())); }); + return pathCells; + } + + public string GetWreckIDTag(string originalTag, Submarine wreck) + { + string shortSeed = ToolBox.StringToInt(LevelData.Seed + wreck?.Info.Name).ToString(); + if (shortSeed.Length > 6) { shortSeed = shortSeed.Substring(0, 6); } + return originalTag + "_" + shortSeed; } public bool IsCloseToStart(Vector2 position, float minDist) => IsCloseToStart(position.ToPoint(), minDist); @@ -3049,10 +3177,8 @@ namespace Barotrauma var waypoints = WayPoint.WayPointList.Where(wp => wp.Submarine == null && wp.SpawnType == SpawnType.Path && - wp.WorldPosition.X < EndExitPosition.X && !IsCloseToStart(wp.WorldPosition, minDistance) && - !IsCloseToEnd(wp.WorldPosition, minDistance) - ).ToList(); + !IsCloseToEnd(wp.WorldPosition, minDistance)).ToList(); var subDoc = SubmarineInfo.OpenFile(contentFile.Path); Rectangle subBorders = Submarine.GetBorders(subDoc.Root); @@ -3137,7 +3263,7 @@ namespace Barotrauma } tempSW.Stop(); Debug.WriteLine($"Sub {sub.Info.Name} loaded in { tempSW.ElapsedMilliseconds} (ms)"); - sub.SetPosition(spawnPoint, forceUndockFromStaticSubmarines: false); + sub.SetPosition(spawnPoint); wreckPositions.Add(sub, positions); blockedRects.Add(sub, rects); return sub; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs index e4288f792..1d6643992 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -84,25 +84,42 @@ namespace Barotrauma objectGrid = new List[ level.Size.X / GridSize, (level.Size.Y - level.BottomPos) / GridSize]; - + List availableSpawnPositions = new List(); var levelCells = level.GetAllCells(); - availableSpawnPositions.AddRange(GetAvailableSpawnPositions(levelCells, LevelObjectPrefab.SpawnPosType.Wall)); + availableSpawnPositions.AddRange(GetAvailableSpawnPositions(levelCells, LevelObjectPrefab.SpawnPosType.Wall)); availableSpawnPositions.AddRange(GetAvailableSpawnPositions(level.SeaFloor.Cells, LevelObjectPrefab.SpawnPosType.SeaFloor)); - - foreach (RuinGeneration.Ruin ruin in level.Ruins) + + foreach (Structure structure in Structure.WallList) { - foreach (var ruinShape in ruin.RuinShapes) + if (!structure.HasBody || structure.HiddenInGame) { continue; } + if (level.Ruins.Any(r => r.Submarine == structure.Submarine)) { - foreach (var wall in ruinShape.Walls) + if (structure.IsHorizontal) { + bool topHull = Hull.FindHull(structure.WorldPosition + Vector2.UnitY * 64) != null; + bool bottomHull = Hull.FindHull(structure.WorldPosition - Vector2.UnitY * 64) != null; + if (topHull && bottomHull ) { continue; } + availableSpawnPositions.Add(new SpawnPosition( - new GraphEdge(wall.A, wall.B), - (wall.A + wall.B) / 2.0f - ruinShape.Center, + new GraphEdge(new Vector2(structure.WorldRect.X, structure.WorldPosition.Y), new Vector2(structure.WorldRect.Right, structure.WorldPosition.Y)), + bottomHull ? Vector2.UnitY : -Vector2.UnitY, LevelObjectPrefab.SpawnPosType.RuinWall, - ruinShape.GetLineAlignment(wall))); + bottomHull ? Alignment.Bottom : Alignment.Top)); } - } + else + { + bool rightHull = Hull.FindHull(structure.WorldPosition + Vector2.UnitX * 64) != null; + bool leftHull = Hull.FindHull(structure.WorldPosition - Vector2.UnitX * 64) != null; + if (rightHull && leftHull) { continue; } + + availableSpawnPositions.Add(new SpawnPosition( + new GraphEdge(new Vector2(structure.WorldPosition.X, structure.WorldRect.Y), new Vector2(structure.WorldPosition.X, structure.WorldRect.Y - structure.WorldRect.Height)), + leftHull ? Vector2.UnitX : -Vector2.UnitX, + LevelObjectPrefab.SpawnPosType.RuinWall, + leftHull ? Alignment.Left : Alignment.Right)); + } + } } foreach (var posOfInterest in level.PositionsOfInterest) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs index 55850a421..3b401aedb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs @@ -13,7 +13,7 @@ namespace Barotrauma partial class LevelTrigger { [Flags] - enum TriggererType + public enum TriggererType { None = 0, Human = 1, @@ -258,7 +258,11 @@ namespace Barotrauma { DebugConsole.ThrowError("Error in LevelTrigger config: \"" + triggeredByStr + "\" is not a valid triggerer type."); } - UpdateCollisionCategories(); + if (PhysicsBody != null) + { + PhysicsBody.CollidesWith = GetCollisionCategories(triggeredBy); + } + TriggerOthersDistance = element.GetAttributeFloat("triggerothersdistance", 0.0f); var tagsArray = element.GetAttributeStringArray("tags", new string[0]); @@ -276,26 +280,17 @@ namespace Barotrauma } } + string debugName = string.IsNullOrEmpty(parentDebugName) ? "LevelTrigger" : $"LevelTrigger in {parentDebugName}"; foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "statuseffect": - statusEffects.Add(StatusEffect.Load(subElement, string.IsNullOrEmpty(parentDebugName) ? "LevelTrigger" : "LevelTrigger in "+ parentDebugName)); + LoadStatusEffect(statusEffects, subElement, debugName); break; case "attack": case "damage": - var attack = new Attack(subElement, string.IsNullOrEmpty(parentDebugName) ? "LevelTrigger" : "LevelTrigger in " + parentDebugName); - if (!triggerOnce) - { - var multipliedAfflictions = attack.GetMultipliedAfflictions((float)Timing.Step); - attack.Afflictions.Clear(); - foreach (Affliction affliction in multipliedAfflictions) - { - attack.Afflictions.Add(affliction, null); - } - } - attacks.Add(attack); + LoadAttack(subElement, debugName, triggerOnce, attacks); break; } } @@ -304,16 +299,13 @@ namespace Barotrauma randomTriggerTimer = Rand.Range(0.0f, randomTriggerInterval); } - private void UpdateCollisionCategories() + public static Category GetCollisionCategories(TriggererType triggeredBy) { - if (PhysicsBody == null) return; - var collidesWith = Physics.CollisionNone; if (triggeredBy.HasFlag(TriggererType.Human) || triggeredBy.HasFlag(TriggererType.Creature)) { collidesWith |= Physics.CollisionCharacter; } if (triggeredBy.HasFlag(TriggererType.Item)) { collidesWith |= Physics.CollisionItem | Physics.CollisionProjectile; } if (triggeredBy.HasFlag(TriggererType.Submarine)) { collidesWith |= Physics.CollisionWall; } - - PhysicsBody.CollidesWith = collidesWith; + return collidesWith; } private void CalculateDirectionalForce() @@ -326,33 +318,31 @@ namespace Barotrauma -sa * unrotatedForce.X + ca * unrotatedForce.Y); } - private bool PhysicsBody_OnCollision(Fixture fixtureA, Fixture fixtureB, FarseerPhysics.Dynamics.Contacts.Contact contact) + public static void LoadStatusEffect(List statusEffects, XElement element, string parentDebugName) + { + statusEffects.Add(StatusEffect.Load(element, parentDebugName)); + } + + public static void LoadAttack(XElement element, string parentDebugName, bool triggerOnce, List attacks) + { + var attack = new Attack(element, parentDebugName); + if (!triggerOnce) + { + var multipliedAfflictions = attack.GetMultipliedAfflictions((float)Timing.Step); + attack.Afflictions.Clear(); + foreach (Affliction affliction in multipliedAfflictions) + { + attack.Afflictions.Add(affliction, null); + } + } + attacks.Add(attack); + } + + private bool PhysicsBody_OnCollision(Fixture fixtureA, Fixture fixtureB, Contact contact) { Entity entity = GetEntity(fixtureB); - if (entity == null) return false; - - if (entity is Character character) - { - if (character.CurrentHull != null) return false; - if (character.IsHuman) - { - if (!triggeredBy.HasFlag(TriggererType.Human)) return false; - } - else - { - if (!triggeredBy.HasFlag(TriggererType.Creature)) return false; - } - } - else if (entity is Item item) - { - if (item.CurrentHull != null) return false; - if (!triggeredBy.HasFlag(TriggererType.Item)) return false; - } - else if (entity is Submarine) - { - if (!triggeredBy.HasFlag(TriggererType.Submarine)) return false; - } - + if (entity == null) { return false; } + if (!IsTriggeredByEntity(entity, triggeredBy, mustBeOutside: true)) { return false; } if (!triggerers.Contains(entity)) { if (!IsTriggered) @@ -365,6 +355,34 @@ namespace Barotrauma return true; } + public static bool IsTriggeredByEntity(Entity entity, TriggererType triggeredBy, bool mustBeOutside = false, (bool mustBe, Submarine sub) mustBeOnSpecificSub = default) + { + if (entity is Character character) + { + if (mustBeOutside && character.CurrentHull != null) { return false; } + if (mustBeOnSpecificSub.mustBe && character.Submarine != mustBeOnSpecificSub.sub) { return false; } + if (character.IsHuman) + { + if (!triggeredBy.HasFlag(TriggererType.Human)) { return false; } + } + else + { + if (!triggeredBy.HasFlag(TriggererType.Creature)) { return false; } + } + } + else if (entity is Item item) + { + if (mustBeOutside && item.CurrentHull != null) { return false; } + if (mustBeOnSpecificSub.mustBe && item.Submarine != mustBeOnSpecificSub.sub) { return false; } + if (!triggeredBy.HasFlag(TriggererType.Item)) { return false; } + } + else if (entity is Submarine) + { + if (!triggeredBy.HasFlag(TriggererType.Submarine)) { return false; } + } + return true; + } + private void PhysicsBody_OnSeparation(Fixture fixtureA, Fixture fixtureB, Contact contact) { Entity entity = GetEntity(fixtureB); @@ -379,10 +397,21 @@ namespace Barotrauma return; } + if (CheckContactsForOtherFixtures(PhysicsBody, fixtureB, entity)) { return; } + + if (triggerers.Contains(entity)) + { + TriggererPosition.Remove(entity); + triggerers.Remove(entity); + } + } + + public static bool CheckContactsForOtherFixtures(PhysicsBody triggerBody, Fixture otherFixture, Entity separatingEntity) + { //check if there are contacts with any other fixture of the trigger //(the OnSeparation callback happens when two fixtures separate, //e.g. if a body stops touching the circular fixture at the end of a capsule-shaped body) - foreach (Fixture fixture in PhysicsBody.FarseerBody.FixtureList) + foreach (Fixture fixture in triggerBody.FarseerBody.FixtureList) { ContactEdge contactEdge = fixture.Body.ContactList; while (contactEdge != null) @@ -393,30 +422,24 @@ namespace Barotrauma { if (contactEdge.Contact.FixtureA != fixture && contactEdge.Contact.FixtureB != fixture) { - var otherEntity = GetEntity(contactEdge.Contact.FixtureB == fixtureB ? + var otherEntity = GetEntity(contactEdge.Contact.FixtureB == otherFixture ? contactEdge.Contact.FixtureB : contactEdge.Contact.FixtureA); - if (otherEntity == entity) { return; } + if (otherEntity == separatingEntity) { return true; } } } contactEdge = contactEdge.Next; } } - - if (triggerers.Contains(entity)) - { - TriggererPosition.Remove(entity); - triggerers.Remove(entity); - } + return false; } - private Entity GetEntity(Fixture fixture) + public static Entity GetEntity(Fixture fixture) { if (fixture.Body == null || fixture.Body.UserData == null) { return null; } if (fixture.Body.UserData is Entity entity) { return entity; } if (fixture.Body.UserData is Limb limb) { return limb.character; } if (fixture.Body.UserData is SubmarineBody subBody) { return subBody.Submarine; } - return null; } @@ -452,15 +475,7 @@ namespace Barotrauma triggerers.RemoveWhere(t => t.Removed); - if (PhysicsBody != null) - { - //failsafe to ensure triggerers get removed when they're far from the trigger - float maxExtent = Math.Max(ConvertUnits.ToDisplayUnits(PhysicsBody.GetMaxExtent() * 5), 5000.0f); - triggerers.RemoveWhere(t => - { - return Vector2.Distance(t.WorldPosition, WorldPosition) > maxExtent; - }); - } + RemoveDistantTriggerers(PhysicsBody, triggerers, WorldPosition); bool isNotClient = true; #if CLIENT @@ -525,57 +540,15 @@ namespace Barotrauma foreach (Entity triggerer in triggerers) { - foreach (StatusEffect effect in statusEffects) - { - if (effect.type == ActionType.OnBroken) { continue; } - Vector2? position = null; - if (effect.HasTargetType(StatusEffect.TargetType.This)) { position = WorldPosition; } - if (triggerer is Character character) - { - effect.Apply(effect.type, deltaTime, triggerer, character, position); - if (effect.HasTargetType(StatusEffect.TargetType.Contained) && character.Inventory != null) - { - foreach (Item item in character.Inventory.AllItemsMod) - { - if (item.ContainedItems == null) { continue; } - foreach (Item containedItem in item.ContainedItems) - { - effect.Apply(effect.type, deltaTime, triggerer, containedItem.AllPropertyObjects, position); - } - } - } - } - else if (triggerer is Item item) - { - effect.Apply(effect.type, deltaTime, triggerer, item.AllPropertyObjects, position); - } - if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) || - effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) - { - targets.Clear(); - targets.AddRange(effect.GetNearbyTargets(worldPosition, targets)); - effect.Apply(effect.type, deltaTime, triggerer, targets); - } - } + ApplyStatusEffects(statusEffects, worldPosition, triggerer, deltaTime, targets); if (triggerer is IDamageable damageable) { - foreach (Attack attack in attacks) - { - attack.DoDamage(null, damageable, WorldPosition, deltaTime, false); - } + ApplyAttacks(attacks, damageable, worldPosition, deltaTime); } else if (triggerer is Submarine submarine) { - foreach (Attack attack in attacks) - { - float structureDamage = attack.GetStructureDamage(deltaTime); - if (structureDamage > 0.0f) - { - Explosion.RangedStructureDamage(worldPosition, attack.DamageRange, structureDamage, levelWallDamage: 0.0f); - } - } - + ApplyAttacks(attacks, worldPosition, deltaTime); if (!string.IsNullOrWhiteSpace(InfectIdentifier)) { submarine.AttemptBallastFloraInfection(InfectIdentifier, deltaTime, InfectionChance); @@ -586,16 +559,16 @@ namespace Barotrauma { if (triggerer is Character character) { - ApplyForce(character.AnimController.Collider, deltaTime); + ApplyForce(character.AnimController.Collider); foreach (Limb limb in character.AnimController.Limbs) { if (limb.IsSevered) { continue; } - ApplyForce(limb.body, deltaTime); + ApplyForce(limb.body); } } else if (triggerer is Submarine submarine) { - ApplyForce(submarine.SubBody.Body, deltaTime); + ApplyForce(submarine.SubBody.Body); } } @@ -606,12 +579,84 @@ namespace Barotrauma } } - private void ApplyForce(PhysicsBody body, float deltaTime) + public static void RemoveDistantTriggerers(PhysicsBody physicsBody, HashSet triggerers, Vector2 calculateDistanceTo) + { + //failsafe to ensure triggerers get removed when they're far from the trigger + if (physicsBody == null) { return; } + float maxExtent = Math.Max(ConvertUnits.ToDisplayUnits(physicsBody.GetMaxExtent() * 5), 5000.0f); + triggerers.RemoveWhere(t => + { + return Vector2.Distance(t.WorldPosition, calculateDistanceTo) > maxExtent; + }); + } + + public static void ApplyStatusEffects(List statusEffects, Vector2 worldPosition, Entity triggerer, float deltaTime, List targets) + { + foreach (StatusEffect effect in statusEffects) + { + if (effect.type == ActionType.OnBroken) { return; } + Vector2? position = null; + if (effect.HasTargetType(StatusEffect.TargetType.This)) { position = worldPosition; } + if (triggerer is Character character) + { + effect.Apply(effect.type, deltaTime, triggerer, character, position); + if (effect.HasTargetType(StatusEffect.TargetType.Contained) && character.Inventory != null) + { + foreach (Item item in character.Inventory.AllItemsMod) + { + if (item.ContainedItems == null) { continue; } + foreach (Item containedItem in item.ContainedItems) + { + effect.Apply(effect.type, deltaTime, triggerer, containedItem.AllPropertyObjects, position); + } + } + } + } + else if (triggerer is Item item) + { + effect.Apply(effect.type, deltaTime, triggerer, item.AllPropertyObjects, position); + } + if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) || effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) + { + targets.Clear(); + targets.AddRange(effect.GetNearbyTargets(worldPosition, targets)); + effect.Apply(effect.type, deltaTime, triggerer, targets); + } + } + } + + /// + /// Applies attacks to a damageable. + /// + public static void ApplyAttacks(List attacks, IDamageable damageable, Vector2 worldPosition, float deltaTime) + { + foreach (Attack attack in attacks) + { + attack.DoDamage(null, damageable, worldPosition, deltaTime, false); + } + } + + /// + /// Applies attacks to structures. + /// + public static void ApplyAttacks(List attacks, Vector2 worldPosition, float deltaTime) + { + foreach (Attack attack in attacks) + { + float structureDamage = attack.GetStructureDamage(deltaTime); + if (structureDamage > 0.0f) + { + Explosion.RangedStructureDamage(worldPosition, attack.DamageRange, structureDamage, levelWallDamage: 0.0f); + } + } + } + + private void ApplyForce(PhysicsBody body) { float distFactor = 1.0f; if (ForceFalloff) { - distFactor = 1.0f - ConvertUnits.ToDisplayUnits(Vector2.Distance(body.SimPosition, PhysicsBody.SimPosition)) / ColliderRadius; + distFactor = GetDistanceFactor(body, PhysicsBody, ColliderRadius); if (distFactor < 0.0f) return; } @@ -648,6 +693,11 @@ namespace Barotrauma } } + public static float GetDistanceFactor(PhysicsBody triggererBody, PhysicsBody triggerBody, float colliderRadius) + { + return 1.0f - ConvertUnits.ToDisplayUnits(Vector2.Distance(triggererBody.SimPosition, triggerBody.SimPosition)) / colliderRadius; + } + public Vector2 GetWaterFlowVelocity(Vector2 viewPosition) { Vector2 baseVel = GetWaterFlowVelocity(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/BTRoom.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/BTRoom.cs deleted file mode 100644 index 1537134e1..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/BTRoom.cs +++ /dev/null @@ -1,190 +0,0 @@ -using Microsoft.Xna.Framework; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Barotrauma.RuinGeneration -{ - /// - /// nodes of a binary tree used for generating underwater "dungeons" - /// - class BTRoom : RuinShape - { - private BTRoom[] subRooms; - - public BTRoom Parent - { - get; - private set; - } - - public Corridor Corridor - { - get; - set; - } - - public BTRoom[] SubRooms - { - get { return subRooms; } - } - - public BTRoom Adjacent - { - get; - private set; - } - - public BTRoom(Rectangle rect) - { - this.rect = rect; - } - - public void Split(float minDivRatio, float verticalProbability = 0.5f, int minWidth = 200, int minHeight = 200) - { - bool verticalSplit = Rand.Range(0.0f, rect.Height / (float)rect.Width, Rand.RandSync.Server) < verticalProbability; - if (rect.Width * minDivRatio < minWidth && rect.Height * minDivRatio < minHeight) - { - minDivRatio = 0.5f; - } - else if (rect.Width * minDivRatio < minWidth) - { - verticalSplit = false; - } - else if (rect.Height * minDivRatio < minHeight) - { - verticalSplit = true; - } - - subRooms = new BTRoom[2]; - if (verticalSplit) - { - SplitVertical(minDivRatio); - } - else - { - SplitHorizontal(minDivRatio); - } - - subRooms[0].Parent = this; - subRooms[1].Parent = this; - - subRooms[0].Adjacent = subRooms[1]; - subRooms[1].Adjacent = subRooms[0]; - } - - private void SplitHorizontal(float minDivRatio) - { - float div = Rand.Range(minDivRatio, 1.0f - minDivRatio, Rand.RandSync.Server); - subRooms[0] = new BTRoom(new Rectangle(rect.X, rect.Y, rect.Width, (int)(rect.Height * div))); - subRooms[1] = new BTRoom(new Rectangle(rect.X, rect.Y + subRooms[0].rect.Height, rect.Width, rect.Height - subRooms[0].rect.Height)); - - } - - private void SplitVertical(float minDivRatio) - { - float div = Rand.Range(minDivRatio, 1.0f - minDivRatio, Rand.RandSync.Server); - subRooms[0] = new BTRoom(new Rectangle(rect.X, rect.Y, (int)(rect.Width * div), rect.Height)); - subRooms[1] = new BTRoom(new Rectangle(rect.X + subRooms[0].rect.Width, rect.Y, rect.Width - subRooms[0].rect.Width, rect.Height)); - } - - public override void CreateWalls() - { - Walls = new List - { - new Line(new Vector2(Rect.X, Rect.Y), new Vector2(Rect.Right, Rect.Y)), - new Line(new Vector2(Rect.X, Rect.Bottom), new Vector2(Rect.Right, Rect.Bottom)), - new Line(new Vector2(Rect.X, Rect.Y), new Vector2(Rect.X, Rect.Bottom)), - new Line(new Vector2(Rect.Right, Rect.Y), new Vector2(Rect.Right, Rect.Bottom)) - }; - } - - public void Scale(Vector2 scale) - { - rect.Inflate((scale.X - 1.0f) * 0.5f * rect.Width, (scale.Y - 1.0f) * 0.5f * rect.Height); - } - - public List GetLeaves() - { - return GetLeaves(new List()); - } - - private List GetLeaves(List leaves) - { - if (subRooms == null) - { - leaves.Add(this); - } - else - { - subRooms[0].GetLeaves(leaves); - subRooms[1].GetLeaves(leaves); - } - - return leaves; - } - - public void GenerateCorridors(int minWidth, int maxWidth, List corridors) - { - if (Adjacent != null && Corridor == null) - { - Corridor = new Corridor(this, Rand.Range(minWidth, maxWidth, Rand.RandSync.Server), corridors); - } - - if (subRooms != null) - { - subRooms[0].GenerateCorridors(minWidth, maxWidth, corridors); - subRooms[1].GenerateCorridors(minWidth, maxWidth, corridors); - } - } - - public static void CalculateDistancesFromEntrance(BTRoom entrance, List rooms, List corridors) - { - entrance.CalculateDistanceFromEntrance(0, rooms, new List(corridors)); - } - - private void CalculateDistanceFromEntrance(int currentDist, List rooms, List corridors) - { - DistanceFromEntrance = DistanceFromEntrance == 0 ? currentDist : Math.Min(currentDist, DistanceFromEntrance); - - currentDist++; - - var roomRect = Rect; - roomRect.Inflate(5, 5); - foreach (var corridor in corridors) - { - var corridorRect = corridor.Rect; - corridorRect.Inflate(5, 5); - if (!corridorRect.Intersects(roomRect)) continue; - - corridor.DistanceFromEntrance = corridor.DistanceFromEntrance == 0 ? - DistanceFromEntrance + 1 : - Math.Min(corridor.DistanceFromEntrance, DistanceFromEntrance + 1); - - - List connectedRooms = new List(); - foreach (var otherRoom in rooms) - { - if (otherRoom == this) continue; - if (otherRoom.DistanceFromEntrance > 0 && otherRoom.DistanceFromEntrance < currentDist) continue; - - var otherRoomRect = otherRoom.Rect; - otherRoomRect.Inflate(5, 5); - if (corridorRect.Intersects(otherRoomRect)) { connectedRooms.Add(otherRoom); } - } - - connectedRooms.Sort((r1, r2) => - { - return - (Math.Abs(r1.Rect.Center.X - Rect.Center.X) + Math.Abs(r1.Rect.Center.Y - Rect.Center.Y)) - - (Math.Abs(r2.Rect.Center.X - Rect.Center.X) + Math.Abs(r2.Rect.Center.Y - Rect.Center.Y)); - }); - - for (int i = 0; i < connectedRooms.Count; i++) - { - connectedRooms[i].CalculateDistanceFromEntrance(currentDist + 1 + i, rooms, corridors); - } - } - } - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/Corridor.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/Corridor.cs deleted file mode 100644 index 7380256ef..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/Corridor.cs +++ /dev/null @@ -1,210 +0,0 @@ -using Microsoft.Xna.Framework; -using System; -using System.Collections.Generic; - -namespace Barotrauma.RuinGeneration -{ - - class Corridor : RuinShape - { - private readonly bool isHorizontal; - - public bool IsHorizontal - { - get { return isHorizontal; } - } - - public BTRoom[] ConnectedRooms - { - get; - private set; - } - - public Corridor(Rectangle rect) - { - this.rect = rect; - - isHorizontal = rect.Width > rect.Height; - } - - public Corridor(BTRoom room, int width, List corridors) - { - System.Diagnostics.Debug.Assert(room.Adjacent != null); - - ConnectedRooms = new BTRoom[2]; - ConnectedRooms[0] = room; - ConnectedRooms[1] = room.Adjacent; - - Rectangle room1, room2; - - room1 = room.Rect; - room2 = room.Adjacent.Rect; - - isHorizontal = (room1.Right <= room2.X || room2.Right <= room1.X); - - //use the leaves as starting points for the corridor - if (room.SubRooms != null) - { - var leaves1 = room.GetLeaves(); - var leaves2 = room.Adjacent.GetLeaves(); - - var suitableLeaves = GetSuitableLeafRooms(leaves1, leaves2, width, isHorizontal); - if (suitableLeaves == null || suitableLeaves.Length < 2) - { - // No suitable leaves found due to intersections - //DebugConsole.ThrowError("Error while generating ruins. Could not find a suitable position for a corridor. The width of the corridors may be too large compared to the sizes of the rooms."); - return; - } - else - { - ConnectedRooms[0] = suitableLeaves[0]; - ConnectedRooms[1] = suitableLeaves[1]; - } - } - else - { - rect = CalculateRectangle(room1, room2, width, isHorizontal); - if (rect.Width <= 0 || rect.Height <= 0) - { - DebugConsole.ThrowError("Error while generating ruins. Attempted to create a corridor with a width or height of <= 0"); - return; - } - } - - room.Corridor = this; - room.Adjacent.Corridor = this; - - for (int i = corridors.Count - 1; i >= 0; i--) - { - var corridor = corridors[i]; - - if (corridor.rect.Intersects(this.rect)) - { - if (isHorizontal && corridor.isHorizontal) - { - if (this.rect.Width < corridor.rect.Width) - return; - else - corridors.RemoveAt(i); - } - else if (!isHorizontal && !corridor.isHorizontal) - { - if (this.rect.Height < corridor.rect.Height) - return; - else - corridors.RemoveAt(i); - } - } - } - - corridors.Add(this); - } - - public override void CreateWalls() - { - Walls = new List(); - if (IsHorizontal) - { - Walls.Add(new Line(new Vector2(Rect.X, Rect.Y), new Vector2(Rect.Right, Rect.Y))); - Walls.Add(new Line(new Vector2(Rect.X, Rect.Bottom), new Vector2(Rect.Right, Rect.Bottom))); - } - else - { - Walls.Add(new Line(new Vector2(Rect.X, Rect.Y), new Vector2(Rect.X, Rect.Bottom))); - Walls.Add(new Line(new Vector2(Rect.Right, Rect.Y), new Vector2(Rect.Right, Rect.Bottom))); - } - } - - /// - /// Find two rooms which have two face-two-face walls that we can place a corridor in between - /// - /// - private BTRoom[] GetSuitableLeafRooms(List leaves1, List leaves2, int width, bool isHorizontal) - { - int iOffset = Rand.Int(leaves1.Count, Rand.RandSync.Server); - int jOffset = Rand.Int(leaves2.Count, Rand.RandSync.Server); - - for (int iCount = 0; iCount < leaves1.Count; iCount++) - { - int i = (iCount + iOffset) % leaves1.Count; - - for (int jCount = 0; jCount < leaves2.Count; jCount++) - { - int j = (jCount + jOffset) % leaves2.Count; - - if (isHorizontal) - { - if (leaves1[i].Rect.Y > leaves2[j].Rect.Bottom - width) continue; - if (leaves1[i].Rect.Bottom < leaves2[j].Rect.Y + width) continue; - } - else - { - if (leaves1[i].Rect.X > leaves2[j].Rect.Right - width) continue; - if (leaves1[i].Rect.Right < leaves2[j].Rect.X + width) continue; - } - - // Check if the given corridor rect would intersect over a third room - if (CheckForIntersection(leaves1[i], leaves2[j], leaves1, leaves2, width, isHorizontal)) continue; - - return new BTRoom[] { leaves1[i], leaves2[j] }; - } - } - - return null; - } - - private bool CheckForIntersection(BTRoom potential1, BTRoom potential2, List leaves1, List leaves2, int width, bool isHorizontal) - { - Rectangle potentialCorridorRectangle = CalculateRectangle(potential1.Rect, potential2.Rect, width, isHorizontal); - - if (potentialCorridorRectangle.Width <= 0 || potentialCorridorRectangle.Height <= 0) return true; // Invalid rectangle - - for (int i = 0; i < leaves1.Count; i++) - { - if (leaves1[i] == potential1) continue; - if (potentialCorridorRectangle.Intersects(leaves1[i].Rect)) return true; - } - - for (int i = 0; i < leaves2.Count; i++) - { - if (leaves2[i] == potential2) continue; - if (potentialCorridorRectangle.Intersects(leaves2[i].Rect)) return true; - } - - rect = potentialCorridorRectangle; // Save the rectangle that passes the test - return false; - } - - private Rectangle CalculateRectangle(Rectangle rect1, Rectangle rect2, int width, bool isHorizontal) - { - if (isHorizontal) - { - int left = Math.Min(rect1.Right, rect2.Right); - int right = Math.Max(rect1.X, rect2.X); - - int top = Math.Max(rect1.Y, rect2.Y); - //int bottom = Math.Min(room1.Bottom, room2.Bottom); - int yPos = top;//Rand.Range(top, bottom - width, Rand.RandSync.Server); - - return new Rectangle(left, yPos, right - left, width); - } - else if (rect1.Y > rect2.Bottom || rect2.Y > rect1.Bottom) - { - int left = Math.Max(rect1.X, rect2.X); - int right = Math.Min(rect1.Right, rect2.Right); - - int top = Math.Min(rect1.Bottom, rect2.Bottom); - int bottom = Math.Max(rect1.Y, rect2.Y); - - int xPos = Rand.Range(left, right - width, Rand.RandSync.Server); - - return new Rectangle(xPos, top, width, bottom - top); - } - else - { - DebugConsole.ThrowError("wat"); - return new Rectangle(); - } - } - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs index d84956923..be1f4811e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs @@ -18,9 +18,9 @@ namespace Barotrauma.RuinGeneration Wall, Back, Door, Hatch, Prop } - class RuinGenerationParams : ISerializableEntity + class RuinGenerationParams : OutpostGenerationParams { - public static List List + public static List RuinParams { get { @@ -34,102 +34,14 @@ namespace Barotrauma.RuinGeneration private static List paramsList; - private string filePath; - - private readonly List roomTypeList; - - public string Name => "RuinGenerationParams"; + private readonly string filePath; + + public override string Name => "RuinGenerationParams"; - [Serialize("5000,5000", false), Editable] - public Point SizeMin - { - get; - set; - } - [Serialize("8000,8000", false), Editable] - public Point SizeMax - { - get; - set; - } - [Serialize(3, false, description: "The ruin generation algorithm \"splits\" the ruin area into two, splits these areas again, repeats this for some number of times and creates a room at each of the final split areas. This is value determines the minimum number of times the split is done."), Editable(MinValueInt = 1, MaxValueInt = 10)] - public int RoomDivisionIterationsMin + private RuinGenerationParams(XElement element, string filePath) : base(element, filePath) { - get; - set; - } - - [Serialize(4, false, description: "The ruin generation algorithm \"splits\" the ruin area into two, splits these areas again, repeats this for some number of times and creates a room at each of the final split areas. This is value determines the maximum number of times the split is done."), Editable(MinValueInt = 1, MaxValueInt = 10)] - public int RoomDivisionIterationsMax - { - get; - set; - } - - [Serialize(0.5f, false, description: "The probability for the split algorithm to split the area vertically. High values tend to create tall, vertical rooms, and low values wide, horizontal rooms."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 0.9f)] - public float VerticalSplitProbability - { - get; - set; - } - - [Serialize(400, false, description: "The splitting algorithm attempts to keep the width of the split areas larger than this. If the width of the split areas would be smaller than this after a vertical split, the algorithm would do a horizontal split."), Editable] - public int MinSplitWidth - { - get; - set; - } - [Serialize(400, false, description: "The splitting algorithm attempts to keep the height of the split areas larger than this. If the height of the split areas would be smaller than this after a vertical split, the algorithm would do a horizontal split."), Editable] - public int MinSplitHeight - { - get; - set; - } - - [Serialize("0.5,0.9", false, description: "The minimum and maximum width of a room relative to the areas created by the split algorithm."), Editable] - public Vector2 RoomWidthRange - { - get; - set; - } - [Serialize("0.5,0.9", false, description: "The minimum and maximum height of a room relative to the areas created by the split algorithm."), Editable] - public Vector2 RoomHeightRange - { - get; - set; - } - - [Serialize("200,256", false, description: "The minimum and maximum width of the corridors between rooms."), Editable] - public Point CorridorWidthRange - { - get; - set; - } - - public Dictionary SerializableProperties - { - get; - private set; - } = new Dictionary(); - - public IEnumerable RoomTypeList - { - get { return roomTypeList; } - } - - private RuinGenerationParams(XElement element) - { - roomTypeList = new List(); - - if (element != null) - { - foreach (XElement subElement in element.Elements()) - { - roomTypeList.Add(new RuinRoom(subElement)); - } - } - SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + this.filePath = filePath; } public static RuinGenerationParams GetRandom() @@ -139,7 +51,7 @@ namespace Barotrauma.RuinGeneration if (paramsList.Count == 0) { DebugConsole.ThrowError("No ruin configuration files found in any content package."); - return new RuinGenerationParams(null); + return new RuinGenerationParams(null, null); } return paramsList[Rand.Int(paramsList.Count, Rand.RandSync.Server)]; @@ -151,23 +63,24 @@ namespace Barotrauma.RuinGeneration foreach (ContentFile configFile in GameMain.Instance.GetFilesOfType(ContentType.RuinConfig)) { XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); - if (doc == null) { continue; } - var mainElement = doc.Root; - if (doc.Root.IsOverride()) + if (doc?.Root == null) { continue; } + + foreach (XElement subElement in doc.Root.Elements()) { - mainElement = doc.Root.FirstElement(); - paramsList.Clear(); - DebugConsole.NewMessage($"Overriding all ruin generation parameters using the file {configFile.Path}.", Color.Yellow); + var mainElement = subElement; + if (subElement.IsOverride()) + { + mainElement = subElement.FirstElement(); + paramsList.Clear(); + DebugConsole.NewMessage($"Overriding all ruin generation parameters using the file {configFile.Path}.", Color.Yellow); + } + else if (paramsList.Any()) + { + DebugConsole.NewMessage($"Adding additional ruin generation parameters from file '{configFile.Path}'"); + } + var newParams = new RuinGenerationParams(mainElement, configFile.Path); + paramsList.Add(newParams); } - else if (paramsList.Any()) - { - DebugConsole.NewMessage($"Adding additional ruin generation parameters from file '{configFile.Path}'"); - } - var newParams = new RuinGenerationParams(mainElement) - { - filePath = configFile.Path - }; - paramsList.Add(newParams); } } @@ -185,11 +98,11 @@ namespace Barotrauma.RuinGeneration NewLineOnAttributes = true }; - foreach (RuinGenerationParams generationParams in List) + foreach (RuinGenerationParams generationParams in RuinParams) { foreach (ContentFile configFile in GameMain.Instance.GetFilesOfType(ContentType.RuinConfig)) { - if (configFile.Path != generationParams.filePath) continue; + if (configFile.Path != generationParams.filePath) { continue; } XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); if (doc == null) { continue; } @@ -205,298 +118,4 @@ namespace Barotrauma.RuinGeneration } } } - - class RuinRoom : ISerializableEntity - { - public enum RoomPlacement - { - Any, - First, - Last - } - - public string Name - { - get; - private set; - } - - [Serialize(1.0f, false), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] - public float Commonness { get; private set; } - - public Dictionary SerializableProperties - { - get; - private set; - } = new Dictionary(); - - [Serialize(RoomPlacement.Any, false), Editable] - public RoomPlacement Placement - { - get; - set; - } - - [Serialize(0, false), Editable] - public int PlacementOffset - { - get; - set; - } - - [Serialize(false, false), Editable] - public bool IsCorridor - { - get; - set; - } - - [Serialize(1.0f, false), Editable] - public float MinWaterAmount - { - get; - set; - } - [Serialize(1.0f, false), Editable] - public float MaxWaterAmount - { - get; - set; - } - - private List entityList = new List(); - - public RuinRoom(XElement element) - { - SerializableProperties = SerializableProperty.DeserializeProperties(this, element); - - Name = element.GetAttributeString("name", ""); - - if (element != null) - { - int groupIndex = 0; - LoadEntities(element, ref groupIndex); - } - - void LoadEntities(XElement element2, ref int groupIndex) - { - foreach (XElement subElement in element2.Elements()) - { - if (subElement.Name.ToString().Equals("chooseone", StringComparison.OrdinalIgnoreCase)) - { - groupIndex++; - LoadEntities(subElement, ref groupIndex); - } - else - { - entityList.Add(new RuinEntityConfig(subElement) { SingleGroupIndex = groupIndex }); - } - } - } - } - - public RuinEntityConfig GetRandomEntity(RuinEntityType type, Alignment alignment) - { - var matchingEntities = entityList.FindAll(rs => - rs.Type == type && - rs.Alignment.HasFlag(alignment)); - - if (!matchingEntities.Any()) return null; - - return ToolBox.SelectWeightedRandom( - matchingEntities, - matchingEntities.Select(s => s.Commonness).ToList(), - Rand.RandSync.Server); - } - - public List GetPropList(RuinShape room, Rand.RandSync randSync) - { - Dictionary> propGroups = new Dictionary>(); - foreach (RuinEntityConfig entityConfig in entityList) - { - if (entityConfig.Type != RuinEntityType.Prop) { continue; } - if (room.Rect.Width < entityConfig.MinRoomSize.X || room.Rect.Height < entityConfig.MinRoomSize.Y) { continue; } - if (room.Rect.Width > entityConfig.MaxRoomSize.X || room.Rect.Height > entityConfig.MaxRoomSize.Y) { continue; } - if (!propGroups.ContainsKey(entityConfig.SingleGroupIndex)) - { - propGroups[entityConfig.SingleGroupIndex] = new List(); - } - propGroups[entityConfig.SingleGroupIndex].Add(entityConfig); - } - - List props = new List(); - foreach (KeyValuePair> propGroup in propGroups) - { - if (propGroup.Key == 0) - { - props.AddRange(propGroup.Value); - } - else - { - props.Add(propGroup.Value[Rand.Int(propGroup.Value.Count, randSync)]); - } - } - return props; - } - } - - class RuinEntityConfig : ISerializableEntity - { - public readonly MapEntityPrefab Prefab; - - public enum RelativePlacement - { - SameRoom, - NextRoom, - NextCorridor, - PreviousRoom, - PreviousCorridor, - FirstRoom, - FirstCorridor, - LastRoom, - LastCorridor - } - - public class EntityConnection - { - //which type of room to search for the item to connect to - //sameroom, nextroom, previousroom, firstroom and lastroom are also valid - public string RoomName - { - get; - private set; - } - - public string TargetEntityIdentifier - { - get; - private set; - } - - //Identifier of the item to run the wire from. Only needed in item assemblies to determine which item in the assembly to use. - public string SourceEntityIdentifier - { - get; - private set; - } - - //if set, the connection is done by running a wire from - //(Pair.First = the name of the connection in this item) to (Pair.Second = the name of the connection in the target item) - public Pair WireConnection - { - get; - private set; - } - - public EntityConnection(XElement element) - { - RoomName = element.GetAttributeString("roomname", ""); - TargetEntityIdentifier = element.GetAttributeString("targetentity", ""); - SourceEntityIdentifier = element.GetAttributeString("sourceentity", ""); - foreach (XElement subElement in element.Elements()) - { - if (subElement.Name.ToString().Equals("wire", StringComparison.OrdinalIgnoreCase)) - { - WireConnection = new Pair( - subElement.GetAttributeString("from", ""), - subElement.GetAttributeString("to", "")); - } - } - } - } - - [Serialize(Alignment.Bottom, false), Editable] - public Alignment Alignment { get; private set; } - - [Serialize("0,0", false, description: "Minimum offset from the anchor position, relative to the size of the room." + - " For example, a value of { -0.5,0 } with a Bottom alignment would mean the entity can be placed anywhere between the bottom-left corner of the room and bottom-center."), Editable] - public Vector2 MinOffset { get; private set; } - [Serialize("0,0", false, description: "Maximum offset from the anchor position, relative to the size of the room." + - " For example, a value of { 0.5,0 } with a Bottom alignment would mean the entity can be placed anywhere between the bottom-right corner of the room and bottom-center."), Editable] - public Vector2 MaxOffset { get; private set; } - - [Serialize(RuinEntityType.Prop, false), Editable] - public RuinEntityType Type { get; private set; } - - [Serialize(false, false), Editable] - public bool Expand { get; private set; } - - [Serialize(RelativePlacement.SameRoom, false), Editable] - public RelativePlacement PlacementRelativeToParent { get; private set; } - - [Serialize(1.0f, false), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)] - public float Commonness { get; private set; } - - [Serialize(1, false)] - public int MinAmount { get; private set; } - [Serialize(1, false)] - public int MaxAmount { get; private set; } - - [Serialize("0,0", false)] - public Point MinRoomSize { get; private set; } - - [Serialize("100000,100000", false)] - public Point MaxRoomSize { get; private set; } - - [Serialize("", false)] - public string TargetContainer { get; private set; } - - public List EntityConnections { get; private set; } = new List(); - - - public int SingleGroupIndex; - - private readonly List childEntities = new List(); - - public IEnumerable ChildEntities - { - get { return childEntities; } - } - - public string Name => Prefab == null ? "null" : Prefab.Name; - - public Dictionary SerializableProperties - { - get; - private set; - } = new Dictionary(); - - public RuinEntityConfig(XElement element) - { - string name = element.GetAttributeString("prefab", ""); - Prefab = MapEntityPrefab.Find(name: null, identifier: name); - - if (Prefab == null) - { - DebugConsole.ThrowError("Loading ruin entity config failed - map entity prefab \"" + name + "\" not found."); - return; - } - - SerializableProperties = SerializableProperty.DeserializeProperties(this, element); - - int gIndex = 0; - LoadChildren(element, ref gIndex); - - void LoadChildren(XElement element2, ref int groupIndex) - { - foreach (XElement subElement in element2.Elements()) - { - switch (subElement.Name.ToString().ToLowerInvariant()) - { - case "connection": - case "entityconnection": - EntityConnections.Add(new EntityConnection(subElement)); - break; - case "chooseone": - groupIndex++; - LoadChildren(subElement, ref groupIndex); - break; - default: - childEntities.Add(new RuinEntityConfig(subElement) { SingleGroupIndex = groupIndex }); - break; - } - } - } - } - } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerator.cs index 4b6b241da..6b2fdba38 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerator.cs @@ -1,212 +1,16 @@ -using FarseerPhysics; +using Barotrauma.Extensions; using Microsoft.Xna.Framework; -using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using Voronoi2; -using Barotrauma.Extensions; -using Barotrauma.Items.Components; namespace Barotrauma.RuinGeneration { - abstract class RuinShape - { - protected Rectangle rect; - - public Rectangle Rect - { - get { return rect; } - } - - public int DistanceFromEntrance - { - get; - set; - } - - public Vector2 Center - { - get { return rect.Center.ToVector2(); } - } - - public RuinRoom RoomType; - - public List Walls; - - public virtual void CreateWalls() { } - - public Alignment GetLineAlignment(Line line) - { - if (line.IsHorizontal) - { - if (line.A.Y > rect.Center.Y && line.B.Y > rect.Center.Y) - { - return Alignment.Bottom; - } - else if (line.A.Y < rect.Center.Y && line.B.Y < rect.Center.Y) - { - return Alignment.Top; - } - } - else - { - if (line.A.X < rect.Center.X && line.B.X < rect.Center.X) - { - return Alignment.Left; - } - else if (line.A.X > rect.Center.X && line.B.X > rect.Center.X) - { - return Alignment.Right; - } - } - - return Alignment.Center; - } - - /// - /// Goes through all the walls of the ruin shape and clips off parts that are inside the rectangle - /// - public void SplitWalls(Rectangle rectangle) - { - List newLines = new List(); - - foreach (Line line in Walls) - { - if (!line.IsHorizontal) //vertical line - { - //line doesn't intersect the rectangle - if (rectangle.X > line.A.X || rectangle.Right < line.A.X || - rectangle.Y > line.B.Y || rectangle.Bottom < line.A.Y) - { - newLines.Add(line); - } - //line completely inside the rectangle, no need to create a wall at all - else if (line.A.Y >= rectangle.Y && line.B.Y <= rectangle.Bottom) - { - continue; - } - //point A is within the rectangle -> cut a portion from the top of the line - else if (line.A.Y >= rectangle.Y && line.A.Y <= rectangle.Bottom) - { - newLines.Add(new Line(new Vector2(line.A.X, rectangle.Bottom), line.B)); - } - //point B is within the rectangle -> cut a portion from the bottom of the line - else if (line.B.Y >= rectangle.Y && line.B.Y <= rectangle.Bottom) - { - newLines.Add(new Line(line.A, new Vector2(line.A.X, rectangle.Y))); - } - //rect is in between the lines -> split the line into two - else - { - newLines.Add(new Line(line.A, new Vector2(line.A.X, rectangle.Y))); - newLines.Add(new Line(new Vector2(line.A.X, rectangle.Bottom), line.B)); - } - } - else - { - //line doesn't intersect the rectangle - if (rectangle.X > line.B.X || rectangle.Right < line.A.X || - rectangle.Y > line.A.Y || rectangle.Bottom < line.A.Y) - { - - newLines.Add(line); - } - else if (line.A.X >= rectangle.X && line.B.X <= rectangle.Right) - { - continue; - } - //point A is within the rectangle -> cut a portion from the left side of the line - else if (line.A.X >= rectangle.X && line.A.X <= rectangle.Right) - { - newLines.Add(new Line(new Vector2(rectangle.Right, line.A.Y), line.B)); - } - //point B is within the rectangle -> cut a portion from the right side of the line - else if (line.B.X >= rectangle.X && line.B.X <= rectangle.Right) - { - newLines.Add(new Line(line.A, new Vector2(rectangle.X, line.A.Y))); - } - //rect is in between the lines -> split the line into two - else - { - newLines.Add(new Line(line.A, new Vector2(rectangle.X, line.A.Y))); - newLines.Add(new Line(new Vector2(rectangle.Right, line.A.Y), line.B)); - } - } - } - - Walls = newLines; - } - - public void MirrorX(Vector2 mirrorOrigin) - { - rect.X = (int)(mirrorOrigin.X + (mirrorOrigin.X - rect.Right)); - for (int i = 0; i < Walls.Count; i++) - { - Walls[i].A = new Vector2(mirrorOrigin.X + (mirrorOrigin.X - Walls[i].A.X), Walls[i].A.Y); - Walls[i].B = new Vector2(mirrorOrigin.X + (mirrorOrigin.X - Walls[i].B.X), Walls[i].B.Y); - - if (Walls[i].B.X < Walls[i].A.X) - { - var temp = Walls[i].A.X; - Walls[i].A.X = Walls[i].B.X; - Walls[i].B.X = temp; - } - } - } - } - - class Line - { - public Vector2 A, B; - - public float Radius; - - public bool IsHorizontal - { - get { return Math.Abs(A.Y - B.Y) < Math.Abs(A.X - B.X); } - } - - public Line(Vector2 a, Vector2 b) - { - Debug.Assert(a.X <= b.X); - Debug.Assert(a.Y <= b.Y); - - A = a; - B = b; - } - } - partial class Ruin { - private List rooms; - private List corridors; + private readonly RuinGenerationParams generationParams; - private List walls; - - private List allShapes; - - private RuinGenerationParams generationParams; - - private BTRoom entranceRoom; - - private List ruinEntities = new List(); - private List doors = new List(); - - public IEnumerable RuinEntities - { - get { return ruinEntities; } - } - - public List RuinShapes - { - get { return allShapes; } - } - - public List Walls - { - get { return walls; } - } + public List PathCells = new List(); public Rectangle Area { @@ -214,1107 +18,54 @@ namespace Barotrauma.RuinGeneration private set; } - public Ruin(VoronoiCell closestPathCell, List caveCells, RuinGenerationParams generationParams, Rectangle area, bool mirror = false) + public Submarine Submarine + { + get; + private set; + } + + public Ruin(Level level, RuinGenerationParams generationParams, Location location, Point position, bool mirror = false) + : this(level, generationParams, location.Type, position, mirror) + { + } + + public Ruin(Level level, RuinGenerationParams generationParams, LocationType locationType, Point position, bool mirror = false) { this.generationParams = generationParams; - Area = area; - corridors = new List(); - rooms = new List(); - walls = new List(); - allShapes = new List(); - Generate(closestPathCell, caveCells, area, mirror); + Generate(level, locationType, position, mirror); } - public void Generate(VoronoiCell closestPathCell, List caveCells, Rectangle area, bool mirror = false) + public void Generate(Level level, LocationType locationType, Point position, bool mirror = false) { - corridors.Clear(); - rooms.Clear(); - - int iterations = Rand.Range(generationParams.RoomDivisionIterationsMin, generationParams.RoomDivisionIterationsMax, Rand.RandSync.Server); - float verticalProbability = generationParams.VerticalSplitProbability; - - BTRoom baseRoom = new BTRoom(area); - rooms = new List { baseRoom }; - - for (int i = 0; i < iterations; i++) - { - rooms.ForEach(l => l.Split(0.3f, verticalProbability, generationParams.MinSplitWidth, generationParams.MinSplitHeight)); - rooms = baseRoom.GetLeaves(); - } - - foreach (BTRoom leaf in rooms) - { - leaf.Scale - ( - new Vector2( - Rand.Range(generationParams.RoomWidthRange.X, generationParams.RoomWidthRange.Y, Rand.RandSync.Server), - Rand.Range(generationParams.RoomHeightRange.X, generationParams.RoomHeightRange.Y, Rand.RandSync.Server)) - ); - } - - baseRoom.GenerateCorridors(generationParams.CorridorWidthRange.X, generationParams.CorridorWidthRange.Y, corridors); - - walls = new List(); - rooms.ForEach(leaf => { leaf.CreateWalls(); }); - - //--------------------------- - - float shortestDistance = 0.0f; - foreach (BTRoom leaf in rooms) - { - Vector2 leafPos = leaf.Rect.Center.ToVector2(); - if (mirror) - { - leafPos.X = area.Center.X + (area.Center.X - leafPos.X); - } - float distance = Vector2.Distance(leafPos, closestPathCell.Center); - if (entranceRoom == null || distance < shortestDistance) - { - entranceRoom = leaf; - shortestDistance = distance; - } - } - - rooms.Remove(entranceRoom); - - //--------------------------- - - foreach (BTRoom leaf in rooms) - { - foreach (Corridor corridor in corridors) - { - leaf.SplitWalls(corridor.Rect); - } - - walls.AddRange(leaf.Walls); - } - - foreach (Corridor corridor in corridors) - { - corridor.CreateWalls(); - - foreach (BTRoom leaf in rooms) - { - corridor.SplitWalls(leaf.Rect); - } - - foreach (Corridor corridor2 in corridors) - { - if (corridor == corridor2) continue; - corridor.SplitWalls(corridor2.Rect); - } - walls.AddRange(corridor.Walls); - } - - BTRoom.CalculateDistancesFromEntrance(entranceRoom, rooms, corridors); - GenerateRuinEntities(caveCells, area, mirror); - } - - public class RuinEntity - { - public readonly RuinEntityConfig Config; - public readonly MapEntity Entity; - public readonly MapEntity Parent; - public readonly RuinShape Room; - - public RuinEntity(RuinEntityConfig config, MapEntity entity, RuinShape room, MapEntity parent = null) - { - Config = config; - Entity = entity; - Room = room; - Parent = parent; - } - } - - private void GenerateRuinEntities(List caveCells, Rectangle ruinArea, bool mirror) - { - var entityGrid = Hull.GenerateEntityGrid(new Rectangle(ruinArea.X, ruinArea.Y + ruinArea.Height, ruinArea.Width, ruinArea.Height)); - doors.Clear(); - - allShapes = new List(rooms); - allShapes.AddRange(corridors); + Submarine = OutpostGenerator.Generate(generationParams, locationType, onlyEntrance: false); + Submarine.Info.Name = $"Ruin ({level.Seed})"; + Submarine.Info.Type = SubmarineType.Ruin; + Submarine.TeamID = CharacterTeamType.None; + Submarine.SetPosition(position.ToVector2()); if (mirror) { - foreach (RuinShape shape in allShapes) - { - shape.MirrorX(ruinArea.Center.ToVector2()); - } + Submarine.FlipX(); } - int maxRoomDistanceFromEntrance = rooms.Max(s => s.DistanceFromEntrance); - int maxCorridorDistanceFromEntrance = corridors.Max(s => s.DistanceFromEntrance); + Rectangle worldBorders = Submarine.Borders; + worldBorders.Location += Submarine.WorldPosition.ToPoint(); + Area = new Rectangle(worldBorders.X, worldBorders.Y - worldBorders.Height, worldBorders.Width, worldBorders.Height); - //assign the room types for the first and last rooms - foreach (RuinRoom roomType in generationParams.RoomTypeList) + List subWaypoints = WayPoint.WayPointList.FindAll(wp => wp.Submarine == Submarine); + int interestingPosCount = 0; + foreach (WayPoint wp in subWaypoints) { - RuinShape selectedRoom = null; - switch (roomType.Placement) - { - case RuinRoom.RoomPlacement.First: - //find the room nearest to the entrance - //there may be multiple ones, choose one that hasn't been assigned yet - selectedRoom = roomType.IsCorridor ? FindFirstRoom(corridors, r => r.RoomType == null) : FindFirstRoom(rooms, r => r.RoomType == null); - - break; - case RuinRoom.RoomPlacement.Last: - //find the room furthest to the entrance - //there may be multiple ones, choose one that hasn't been assigned yet - selectedRoom = roomType.IsCorridor ? FindLastRoom(corridors, r => r.RoomType == null) : FindLastRoom(rooms, r => r.RoomType == null); - break; - } - if (selectedRoom == null) continue; - - //step forwards/backwards from the selected room according to the placement offset - for (int i = 0; i < Math.Abs(roomType.PlacementOffset); i++) - { - selectedRoom = FindNearestRoom( - selectedRoom, - roomType.IsCorridor ? corridors : (IEnumerable)rooms, - roomType.PlacementOffset, - r => r.RoomType == null); - } - - if (selectedRoom != null) selectedRoom.RoomType = roomType; + if (wp.SpawnType != SpawnType.Enemy) { continue; } + level.PositionsOfInterest.Add(new Level.InterestingPosition(wp.WorldPosition.ToPoint(), Level.PositionType.Ruin, this)); + interestingPosCount++; } - //go through the unassigned rooms - foreach (RuinShape room in allShapes) + if (interestingPosCount == 0) { - if (room.RoomType != null) continue; - - room.RoomType = generationParams.RoomTypeList.GetRandom(rt => - rt.IsCorridor == room is Corridor && - rt.Placement == RuinRoom.RoomPlacement.Any, - Rand.RandSync.Server); - - if (room.RoomType == null) - { - DebugConsole.ThrowError("Could not find a suitable room type for a room (is corridor: " + (room is Corridor) + ")"); - } - } - - List hullRects = new List(allShapes.Select(s => s.Rect)); - - //split intersecting hulls into multiple parts to prevent overlaps - for (int i = 0; i < hullRects.Count; i++) - { - if (hullRects[i].Width <= 0 || hullRects[i].Height <= 0) continue; - for (int j = 0; j < hullRects.Count; j++) - { - if (i == j) continue; - if (hullRects[j].Width <= 0 || hullRects[j].Height <= 0) continue; - if (!hullRects[i].Intersects(hullRects[j])) continue; - - //hull i goes through hull j vertically - if (hullRects[i].X >= hullRects[j].X && hullRects[i].Right <= hullRects[j].Right && - hullRects[i].Y <= hullRects[j].Y && hullRects[i].Bottom >= hullRects[j].Bottom) - { - Rectangle rectLeft = new Rectangle(hullRects[j].X, hullRects[j].Y, hullRects[i].X - hullRects[j].X, hullRects[j].Height); - Rectangle rectRight = new Rectangle(hullRects[i].Right, hullRects[j].Y, hullRects[j].Right - hullRects[i].Right, hullRects[j].Height); - hullRects[j] = rectLeft; - hullRects.Add(rectRight); - } - else if //hull i goes through hull j horizontally - (hullRects[i].Y >= hullRects[j].Y && hullRects[i].Bottom <= hullRects[j].Bottom && - hullRects[i].X <= hullRects[j].X && hullRects[i].Right >= hullRects[j].Right) - { - Rectangle rectBottom = new Rectangle(hullRects[j].X, hullRects[j].Y, hullRects[j].Width, hullRects[i].Y - hullRects[j].Y); - Rectangle rectTop = new Rectangle(hullRects[j].X, hullRects[i].Bottom, hullRects[j].Width, hullRects[j].Bottom - hullRects[i].Bottom); - hullRects[j] = rectBottom; - hullRects.Add(rectTop); - } - //upper side of hull i is inside hull j - else if (hullRects[j].Contains(hullRects[i].Location) && hullRects[j].Contains(new Vector2(hullRects[i].Right, hullRects[i].Y))) - { - hullRects[i] = new Rectangle(hullRects[i].X, hullRects[j].Bottom, hullRects[i].Width, hullRects[i].Bottom - hullRects[j].Bottom); - } - //lower side of hull i is inside hull j - else if (hullRects[j].Contains(new Vector2(hullRects[i].X, hullRects[i].Bottom)) && hullRects[j].Contains(new Vector2(hullRects[i].Right, hullRects[i].Bottom))) - { - hullRects[i] = new Rectangle(hullRects[i].X, hullRects[i].Y, hullRects[i].Width, hullRects[j].Y - hullRects[i].Y); - } - //left side of hull i is inside hull j - else if (hullRects[j].Contains(hullRects[i].Location) && hullRects[j].Contains(new Vector2(hullRects[i].X, hullRects[i].Bottom))) - { - hullRects[i] = new Rectangle(hullRects[j].X, hullRects[i].Y, hullRects[i].Right - hullRects[j].X, hullRects[i].Height); - } - //right side of hull i is inside hull j - else if (hullRects[j].Contains(new Vector2(hullRects[i].Right, hullRects[i].Y)) && hullRects[j].Contains(new Vector2(hullRects[i].Right, hullRects[i].Bottom))) - { - hullRects[i] = new Rectangle(hullRects[i].X, hullRects[i].Y, hullRects[j].X - hullRects[i].X, hullRects[i].Height); - } - } - } - - foreach (RuinShape room in allShapes) - { - if (room.RoomType == null) continue; - //generate walls -------------------------------------------------------------- - foreach (Line wall in room.Walls) - { - var ruinEntityConfig = room.RoomType.GetRandomEntity(RuinEntityType.Wall, room.GetLineAlignment(wall)); - if (ruinEntityConfig == null) continue; - - wall.Radius = (wall.A.X == wall.B.X) ? - (ruinEntityConfig.Prefab as StructurePrefab).Size.X * 0.5f : - (ruinEntityConfig.Prefab as StructurePrefab).Size.Y * 0.5f; - - Rectangle rect = new Rectangle( - (int)(wall.A.X - wall.Radius), - (int)(wall.B.Y + wall.Radius), - (int)((wall.B.X - wall.A.X) + wall.Radius * 2.0f), - (int)((wall.B.Y - wall.A.Y) + wall.Radius * 2.0f)); - - //cut a section off from both ends of a horizontal wall to get nicer looking corners - if (wall.A.Y == wall.B.Y) - { - rect.Inflate(-32, 0); - if (rect.Width < Submarine.GridSize.X) continue; - } - - var structure = new Structure(rect, ruinEntityConfig.Prefab as StructurePrefab, null) - { - ShouldBeSaved = false - }; - structure.SetCollisionCategory(Physics.CollisionLevel); - CreateChildEntities(ruinEntityConfig, structure, room); - ruinEntities.Add(new RuinEntity(ruinEntityConfig, structure, room)); - } - - //generate backgrounds -------------------------------------------------------------- - var backgroundConfig = room.RoomType.GetRandomEntity(RuinEntityType.Back, Alignment.Center); - if (backgroundConfig != null) - { - Rectangle backgroundRect = new Rectangle(room.Rect.X, room.Rect.Y + room.Rect.Height, room.Rect.Width, room.Rect.Height); - var backgroundStructure = new Structure(backgroundRect, (backgroundConfig.Prefab as StructurePrefab), null) - { - ShouldBeSaved = false - }; - CreateChildEntities(backgroundConfig, backgroundStructure, room); - ruinEntities.Add(new RuinEntity(backgroundConfig, backgroundStructure, room)); - } - - var submarineBlocker = GameMain.World.CreateRectangle( - ConvertUnits.ToSimUnits(room.Rect.Width), - ConvertUnits.ToSimUnits(room.Rect.Height), - 1, ConvertUnits.ToSimUnits(room.Center)); - - submarineBlocker.BodyType = BodyType.Static; - submarineBlocker.CollisionCategories = Physics.CollisionWall; - submarineBlocker.CollidesWith = Physics.CollisionWall; - submarineBlocker.UserData = "ruinroom"; - - //generate doors -------------------------------------------------------------- - if (room is Corridor corridor) - { - var doorConfig = room.RoomType.GetRandomEntity(corridor.IsHorizontal ? RuinEntityType.Door : RuinEntityType.Hatch, Alignment.Center); - if (corridor != null && doorConfig != null) - { - //find all walls that are parallel to the corridor - var suitableWalls = corridor.IsHorizontal ? - corridor.Walls.FindAll(c => c.A.Y == c.B.Y) : corridor.Walls.FindAll(c => c.A.X == c.B.X); - - if (suitableWalls.Any()) - { - //choose a random wall to place the door next to - Vector2 doorPos = corridor.Center; - var wall = suitableWalls[Rand.Int(suitableWalls.Count, Rand.RandSync.Server)]; - if (corridor.IsHorizontal) - { - doorPos.X = (wall.A.X + wall.B.X) / 2.0f; - } - else - { - doorPos.Y = (wall.A.Y + wall.B.Y) / 2.0f; - } - Item doorItem = null; - if (doorConfig.Prefab is ItemPrefab itemPrefab) - { - doorItem = new Item(doorConfig.Prefab as ItemPrefab, doorPos, null) - { - ShouldBeSaved = false - }; - } - else if (doorConfig.Prefab is ItemAssemblyPrefab itemAssemblyPrefab) - { - var entities = itemAssemblyPrefab.CreateInstance(doorPos, sub: null); - foreach (MapEntity e in entities) - { - if (e is Structure) e.ShouldBeSaved = false; - if (doorItem == null && e is Item item && item.GetComponent() != null) - { - doorItem = item; - } - else - { - ruinEntities.Add(new RuinEntity(doorConfig, e, room)); - } - } - if (doorConfig.Expand) { ExpandEntities(entities); } - //make sure the door gets positioned at the correct place regardless of its position in the item assembly - if (doorItem != null) - { - Vector2 doorOffset = doorPos - doorItem.WorldPosition; - foreach (MapEntity e in entities) - { - e.Move(doorOffset); - Door doorComponent = (e as Item)?.GetComponent(); - if (doorComponent != null && !entities.Contains(doorComponent.LinkedGap)) - { - doorComponent.LinkedGap.Move(doorOffset); - } - } - } - } - else - { - DebugConsole.ThrowError("Failed to create a ruin door. Ruin entity \"" + doorConfig.Name + "\" is marked as a door but is neither an item or an item assembly."); - continue; - } - - Door door = doorItem?.GetComponent(); - if (door == null) - { - DebugConsole.ThrowError("Failed to create a ruin door. Door not found in the ruin entity \"" + doorConfig.Name + "\"."); - continue; - } - - CreateChildEntities(doorConfig, doorItem, corridor); - doors.Add(door); - ruinEntities.Add(new RuinEntity(doorConfig, doorItem, room)); - } - } - } - - //generate props -------------------------------------------------------------- - var props = room.RoomType.GetPropList(room, Rand.RandSync.Server); - foreach (RuinEntityConfig prop in props) - { - int amount = Rand.Range(prop.MinAmount, prop.MaxAmount + 1, Rand.RandSync.Server); - for (int i = 0; i < amount; i++) - { - CreateEntity(prop, room, parent: null); - } - } - } - - foreach (RuinEntity entity in ruinEntities) - { - if (!entity.Room.RoomType.IsCorridor) { continue; } - - Item item = entity.Entity as Item; - Door door = item?.GetComponent(); - if (door == null) { continue; } - - //split the hull the door is inside - for (int i = 0; i < hullRects.Count; i++) - { - Vector2 doorPos = door.Item.WorldPosition; - if (!hullRects[i].Contains(doorPos)) continue; - - if (door.IsHorizontal) - { - Rectangle rectBottom = new Rectangle(hullRects[i].X, hullRects[i].Y, hullRects[i].Width, (int)doorPos.Y - hullRects[i].Y); - Rectangle rectTop = new Rectangle(hullRects[i].X, (int)doorPos.Y, hullRects[i].Width, hullRects[i].Bottom - (int)doorPos.Y); - hullRects[i] = rectBottom; - hullRects.Add(rectTop); - } - else - { - Rectangle rectLeft = new Rectangle(hullRects[i].X, hullRects[i].Y, (int)doorPos.X - hullRects[i].X, hullRects[i].Height); - Rectangle rectRight = new Rectangle((int)doorPos.X, hullRects[i].Y, hullRects[i].Right - (int)doorPos.X, hullRects[i].Height); - hullRects[i] = rectLeft; - hullRects.Add(rectRight); - } - break; - } - } - - //randomize door states (20% open on average) - foreach (Door door in doors) - { - door.IsOpen = Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) < 0.2f; - } - - //create connections between all generated entities --------------------------- - foreach (RuinEntity ruinEntity in ruinEntities) - { - CreateConnections(ruinEntity); - } - - foreach (RuinEntity ruinEntity in ruinEntities) - { - if (ruinEntity.Entity is Item item) - { - foreach (ItemComponent ic in item.Components) - { - // Prevent wiring & interacting - if (ic is ConnectionPanel connectionPanel) - { - connectionPanel.Locked = true; - connectionPanel.CanBeSelected = false; - connectionPanel.Item.ShouldBeSaved = false; - } - // Hide wires - if (ic is Wire wire) - { - wire.Hidden = true; - wire.CanBeSelected = false; - wire.Item.ShouldBeSaved = false; - } - } - } - } - - //create hulls --------------------------- - foreach (Rectangle hullRect in hullRects) - { - if (hullRect.Width <= 0 || hullRect.Height <= 0) continue; - var hull = new Hull(MapEntityPrefab.Find(null, "hull"), - new Rectangle(hullRect.X, hullRect.Y + hullRect.Height, hullRect.Width, hullRect.Height), submarine: null) - { - ParentRuin = this, - ShouldBeSaved = false - }; - RuinShape room = allShapes.Find(s => s.Rect.Contains(hullRect.Center)); - if (room?.RoomType != null) - { - hull.WaterVolume = hull.Volume * Rand.Range(room.RoomType.MinWaterAmount, room.RoomType.MaxWaterAmount, Rand.RandSync.Server); - } - entityGrid.InsertEntity(hull); - } - - //create gaps between hulls --------------------------- - hullRects.Add(entranceRoom.Rect); - for (int i = 0; i < hullRects.Count; i++) - { - if (hullRects[i].Width <= 0 || hullRects[i].Height <= 0) continue; - for (int j = i + 1; j < hullRects.Count; j++) - { - Rectangle? gapRect = null; - if (Math.Abs(hullRects[i].X - hullRects[j].Right) <= 1 && hullYIntersect(hullRects[i], hullRects[j])) - { - gapRect = new Rectangle( - hullRects[i].X - 3, Math.Max(hullRects[i].Y, hullRects[j].Y), - 6, Math.Min(hullRects[i].Bottom, hullRects[j].Bottom) - Math.Max(hullRects[i].Y, hullRects[j].Y)); - } - else if (Math.Abs(hullRects[i].Right - hullRects[j].X) <= 1 && hullYIntersect(hullRects[i], hullRects[j])) - { - gapRect = new Rectangle( - hullRects[i].Right - 3, Math.Max(hullRects[i].Y, hullRects[j].Y), - 6, Math.Min(hullRects[i].Bottom, hullRects[j].Bottom) - Math.Max(hullRects[i].Y, hullRects[j].Y)); - } - else if (Math.Abs(hullRects[i].Y - hullRects[j].Bottom) <= 1 && hullXIntersect(hullRects[i], hullRects[j])) - { - gapRect = new Rectangle( - Math.Max(hullRects[i].X, hullRects[j].X), hullRects[i].Y - 3, - Math.Min(hullRects[i].Right, hullRects[j].Right) - Math.Max(hullRects[i].X, hullRects[j].X), 6); - } - else if (Math.Abs(hullRects[i].Bottom - hullRects[j].Y) <= 1 && hullXIntersect(hullRects[i], hullRects[j])) - { - gapRect = new Rectangle( - Math.Max(hullRects[i].X, hullRects[j].X), hullRects[i].Bottom - 3, - Math.Min(hullRects[i].Right, hullRects[j].Right) - Math.Max(hullRects[i].X, hullRects[j].X), 6); - } - - if (!gapRect.HasValue || gapRect.Value.Width <= 0 || gapRect.Value.Height <= 0) continue; - - //doors create their own gaps, don't create an additional one if there's a door at this - bool doorFound = false; - foreach (Item item in Item.ItemList) - { - var door = item.GetComponent(); - if (door == null) { continue; } - if (Math.Abs(door.Item.WorldPosition.X - gapRect.Value.Center.X) < 5 && - Math.Abs(door.Item.WorldPosition.Y - gapRect.Value.Center.Y) < 5) - { - doorFound = true; - break; - } - } - if (doorFound) { continue; } - - new Gap(new Rectangle(gapRect.Value.X, gapRect.Value.Y + gapRect.Value.Height, gapRect.Value.Width, gapRect.Value.Height), - isHorizontal: gapRect.Value.Height > gapRect.Value.Width, submarine: null) - { - ParentRuin = this, - ShouldBeSaved = false - }; - } - } - - foreach (RuinEntity ruinEntity in ruinEntities) - { - ruinEntity.Entity.ParentRuin = this; - } - - bool hullXIntersect(Rectangle rect1, Rectangle rect2) - { - return - (rect1.X >= rect2.X && rect1.X <= rect2.Right) || - (rect2.X >= rect1.X && rect2.X <= rect1.Right); - } - bool hullYIntersect(Rectangle rect1, Rectangle rect2) - { - return - (rect1.Y >= rect2.Y && rect1.Y <= rect2.Bottom) || - (rect2.Y >= rect1.Y && rect2.Y <= rect1.Bottom); + //make sure there's at least on PositionsOfInterest in the ruins + level.PositionsOfInterest.Add(new Level.InterestingPosition(subWaypoints.GetRandom(Rand.RandSync.Server).WorldPosition.ToPoint(), Level.PositionType.Ruin, this)); } } - - private void CreateEntity(RuinEntityConfig entityConfig, RuinShape room, MapEntity parent) - { - if (room == null) return; - - int leftWallThickness = 32, rightWallThickness = 32; - int topWallThickness = 32, bottomWallThickness = 32; - foreach (Line wall in room.Walls) - { - if (wall.IsHorizontal) - { - if (wall.A.Y > room.Rect.Center.Y) - bottomWallThickness = (int)wall.Radius; - else - topWallThickness = (int)wall.Radius; - } - else - { - if (wall.A.X > room.Rect.Center.X) - rightWallThickness = (int)wall.Radius; - else - leftWallThickness = (int)wall.Radius; - } - } - - Rectangle roomBounds = new Rectangle( - room.Rect.X + leftWallThickness, - room.Rect.Y + bottomWallThickness, - room.Rect.Width - leftWallThickness - rightWallThickness, - room.Rect.Height - topWallThickness - bottomWallThickness); - - Vector2 size = Vector2.Zero; - if (entityConfig.Prefab is StructurePrefab structurePrefab) - { - size = structurePrefab.Size; - } - else if (entityConfig.Prefab is ItemPrefab itemPrefab) - { - size = itemPrefab.Size; - } - else if (entityConfig.Prefab is ItemAssemblyPrefab assemblyPrefab) - { - size = new Vector2(assemblyPrefab.Bounds.Width, assemblyPrefab.Bounds.Height); - - Vector2 boundsMin = new Vector2(-assemblyPrefab.Bounds.X, -assemblyPrefab.Bounds.Y); - Vector2 boundsMax = new Vector2(assemblyPrefab.Bounds.Right, assemblyPrefab.Bounds.Bottom); - - roomBounds = new Rectangle( - (int)(roomBounds.X + boundsMin.X), - (int)(roomBounds.Y + boundsMin.Y), - (int)(roomBounds.Width - boundsMin.X - boundsMax.X), - (int)(roomBounds.Height - boundsMin.Y - boundsMax.Y)); - } - - List potentialAnchorPositions = new List(); - if (entityConfig.Alignment.HasFlag(Alignment.Top)) - { - potentialAnchorPositions.Add(new Vector2(roomBounds.Center.X, roomBounds.Bottom)); - } - if (entityConfig.Alignment.HasFlag(Alignment.Bottom)) - { - potentialAnchorPositions.Add(new Vector2(roomBounds.Center.X, roomBounds.Top)); - } - if (entityConfig.Alignment.HasFlag(Alignment.Right)) - { - potentialAnchorPositions.Add(new Vector2(roomBounds.Right, roomBounds.Center.Y)); - } - if (entityConfig.Alignment.HasFlag(Alignment.Left)) - { - potentialAnchorPositions.Add(new Vector2(roomBounds.X, roomBounds.Center.Y)); - } - if (entityConfig.Alignment.HasFlag(Alignment.Center) || potentialAnchorPositions.Count == 0) - { - potentialAnchorPositions.Add(roomBounds.Center.ToVector2()); - } - - Vector2 position = potentialAnchorPositions[Rand.Int(potentialAnchorPositions.Count, Rand.RandSync.Server)]; - Vector2 minPosition = new Vector2( - position.X + entityConfig.MinOffset.X * roomBounds.Width, - position.Y + entityConfig.MinOffset.Y * roomBounds.Height); - Vector2 maxPosition = new Vector2( - position.X + entityConfig.MaxOffset.X * roomBounds.Width, - position.Y + entityConfig.MaxOffset.Y * roomBounds.Height); - - position = new Vector2( - Rand.Range(minPosition.X, maxPosition.X, Rand.RandSync.Server), - Rand.Range(minPosition.Y, maxPosition.Y, Rand.RandSync.Server)); - position.X = MathHelper.Clamp(position.X, roomBounds.X, roomBounds.Right); - position.Y = MathHelper.Clamp(position.Y, roomBounds.Y, roomBounds.Bottom); - - int iterations = 0; - while (iterations < 100) - { - bool overlapFound = false; - foreach (RuinEntity ruinEntity in ruinEntities) - { - if (ruinEntity.Config.Type == RuinEntityType.Back || ruinEntity.Config.Type == RuinEntityType.Wall) continue; - Vector2 diff = position - ruinEntity.Entity.Position; - if (Math.Abs(diff.X) < (size.X + ruinEntity.Entity.Rect.Width) / 2 && - Math.Abs(diff.Y) < (size.Y + ruinEntity.Entity.Rect.Height) / 2) - { - float dist = diff.Length(); - Vector2 moveDir = dist < 0.01f ? Vector2.UnitY : diff / dist; - - position += moveDir * 100.0f; - - position.X = MathHelper.Clamp(position.X, roomBounds.X, roomBounds.Right); - position.Y = MathHelper.Clamp(position.Y, roomBounds.Y, roomBounds.Bottom); - overlapFound = true; - } - } - iterations++; - if (!overlapFound) { break; } - } - - MapEntity entity = null; - if (entityConfig.Prefab is ItemPrefab) - { - Item container = null; - if (entityConfig.TargetContainer != "") - { - List roomContents = ruinEntities.FindAll(re => re.Room == room); - for (int j = 0; j < roomContents.Count; j++) - { - if (roomContents[j].Entity is Item && (roomContents[j].Entity as Item).HasTag(entityConfig.TargetContainer)) - { - container = roomContents[j].Entity as Item; - break; - } - } - - if (container == null) DebugConsole.ThrowError("No container with tag \"" + entityConfig.TargetContainer + "\" found, placing item in the room"); - } - - if (container != null) - { - entity = new Item((ItemPrefab)entityConfig.Prefab, container.Position, null); - if (container.OwnInventory.TryPutItem(entity as Item, null, createNetworkEvent: false)) - { - CreateChildEntities(entityConfig, entity, room); - ruinEntities.Add(new RuinEntity(entityConfig, entity, room, parent)); - } - else // Removing items that don't fit in the container - { - entity.Remove(); - } - } - else - { - entity = new Item((ItemPrefab)entityConfig.Prefab, position, null); - CreateChildEntities(entityConfig, entity, room); - ruinEntities.Add(new RuinEntity(entityConfig, entity, room, parent)); - } - } - else if (entityConfig.Prefab is ItemAssemblyPrefab itemAssemblyPrefab) - { - var entities = itemAssemblyPrefab.CreateInstance(position, sub: null); - foreach (MapEntity e in entities) - { - if (e is Structure) - { - e.ShouldBeSaved = false; - } - else if (e is Item item) - { - var door = item.GetComponent(); - if (door != null) { doors.Add(door); } - } - ruinEntities.Add(new RuinEntity(entityConfig, e, room, parent)); - } - if (entityConfig.Expand) - { - ExpandEntities(entities); - } - CreateChildEntities(entityConfig, entity, room); - } - else - { - entity = new Structure(new Rectangle( - (int)(position.X - size.X / 2.0f), (int)(position.Y + size.Y / 2.0f), - (int)size.X, (int)size.Y), - entityConfig.Prefab as StructurePrefab, null) - { - ShouldBeSaved = false - }; - if (entityConfig.Expand) - { - ExpandEntities(new List() { entity }); - } - CreateChildEntities(entityConfig, entity, room); - ruinEntities.Add(new RuinEntity(entityConfig, entity, room, parent)); - } - } - - private void CreateChildEntities(RuinEntityConfig parentEntityConfig, MapEntity parentEntity, RuinShape room, Rand.RandSync randSync = Rand.RandSync.Server) - { - Dictionary> propGroups = new Dictionary>(); - foreach (RuinEntityConfig entityConfig in parentEntityConfig.ChildEntities) - { - if (!propGroups.ContainsKey(entityConfig.SingleGroupIndex)) - { - propGroups[entityConfig.SingleGroupIndex] = new List(); - } - propGroups[entityConfig.SingleGroupIndex].Add(entityConfig); - } - - List props = new List(); - foreach (KeyValuePair> propGroup in propGroups) - { - if (propGroup.Key == 0) - { - props.AddRange(propGroup.Value); - } - else - { - props.Add(propGroup.Value[Rand.Int(propGroup.Value.Count, randSync)]); - } - } - - foreach (RuinEntityConfig childEntity in props) - { - var childRoom = FindRoom(childEntity.PlacementRelativeToParent, room); - if (childRoom != null) - { - int amount = Rand.Range(childEntity.MinAmount, childEntity.MaxAmount + 1, Rand.RandSync.Server); - for (int i = 0; i < amount; i++) - { - CreateEntity(childEntity, childRoom, parentEntity); - } - } - } - } - - private void CreateConnections(RuinEntity entity) - { - foreach (RuinEntityConfig.EntityConnection connection in entity.Config.EntityConnections) - { - if (!string.IsNullOrEmpty(connection.SourceEntityIdentifier) && - connection.SourceEntityIdentifier != entity.Entity?.prefab.Identifier) - { - continue; - } - - MapEntity targetEntity = null; - if (connection.TargetEntityIdentifier == "parent") - { - targetEntity = entity.Parent; - } - else if (!string.IsNullOrEmpty(connection.RoomName)) - { - RuinShape targetRoom = null; - if (Enum.TryParse(connection.RoomName, out RuinEntityConfig.RelativePlacement placement)) - { - targetRoom = FindRoom(placement, entity.Room); - } - else - { - targetRoom = allShapes.Find(s => s.RoomType?.Name == connection.RoomName); - } - - if (targetRoom == null) - { - DebugConsole.ThrowError("Error while generating ruins - could not find a room of the type \"" + connection.RoomName + "\"."); - } - else - { - targetEntity = ruinEntities.GetRandom(e => - e.Room == targetRoom && - e.Entity.prefab?.Identifier == connection.TargetEntityIdentifier, Rand.RandSync.Server)?.Entity; - } - } - else - { - targetEntity = ruinEntities.GetRandom(e => e.Entity.prefab?.Identifier == connection.TargetEntityIdentifier, Rand.RandSync.Server)?.Entity; - } - - if (targetEntity == null) continue; - - if (connection.WireConnection != null) - { - Item item = entity.Entity as Item; - if (item == null) - { - DebugConsole.ThrowError("Could not connect a wire to the ruin entity \"" + entity.Entity.Name + "\" - the entity is not an item."); - continue; - } - else if (item.Connections == null) - { - DebugConsole.ThrowError("Could not connect a wire to the ruin entity \"" + entity.Entity.Name + "\" - the item does not have a connection panel component."); - continue; - } - - Item parentItem = entity.Parent as Item; - if (parentItem == null) - { - DebugConsole.ThrowError("Could not connect a wire to the ruin entity \"" + parentItem.Name + "\" - the entity is not an item."); - continue; - } - else if (parentItem.Connections == null) - { - DebugConsole.ThrowError("Could not connect a wire to the ruin entity \"" + parentItem.Name + "\" - the item does not have a connection panel component."); - continue; - } - - //TODO: alien wire prefab w/ custom sprite? - var wirePrefab = MapEntityPrefab.Find(null, "blackwire") as ItemPrefab; - - var conn1 = item.Connections.Find(c => c.Name == connection.WireConnection.First); - if (conn1 == null) - { - DebugConsole.ThrowError("Could not connect a wire to the ruin entity \"" + item.Name + - "\" - the item does not have a connection named \"" + connection.WireConnection.First + "\"."); - continue; - } - var conn2 = parentItem.Connections.Find(c => c.Name == connection.WireConnection.Second); - if (conn2 == null) - { - DebugConsole.ThrowError("Could not connect a wire to the ruin entity \"" + parentItem.Name + - "\" - the item does not have a connection named \"" + connection.WireConnection.Second + "\"."); - continue; - } - - var wire = new Item(wirePrefab, parentItem.WorldPosition, null).GetComponent(); - wire.Item.ShouldBeSaved = false; - conn1.TryAddLink(wire); - wire.Connect(conn1, true); - conn2.TryAddLink(wire); - wire.Connect(conn2, true); - wire.Hidden = true; // Hidden for now - } - else - { - entity.Entity.linkedTo.Add(targetEntity); - targetEntity.linkedTo.Add(entity.Entity); - } - } - } - - private void ExpandEntities(IEnumerable entities) - { - Vector2 xBounds = new Vector2(entities.Min(e => e.Rect.X), entities.Max(e => e.Rect.Right)); - Vector2 yBounds = new Vector2(entities.Min(e => e.Rect.Y - e.Rect.Height), entities.Max(e => e.Rect.Y)); - Vector2 center = new Vector2((xBounds.X + xBounds.Y) / 2.0f, (yBounds.X + yBounds.Y) / 2.0f); - - foreach (MapEntity entity in entities) - { - if (entity is Item item) - { - Vector2 moveTo = StretchPoint(entity.WorldPosition, center, xBounds, yBounds); - Vector2 moveAmount = moveTo - entity.WorldPosition; - var connectionPanel = item.GetComponent(); - connectionPanel?.MoveConnectedWires(moveAmount); - entity.Move(moveAmount); - } - else if (entity is Structure structure) - { - if (!entity.ResizeHorizontal && !entity.ResizeVertical) - { - Vector2 moveTo = StretchPoint(entity.WorldPosition, center, xBounds, yBounds); - entity.Move(moveTo - entity.WorldPosition); - continue; - } - - Vector2 structureBoundsMin = new Vector2(structure.Rect.X, structure.Rect.Y - structure.Rect.Height); - Vector2 structureBoundsMax = new Vector2(structure.Rect.Right, structure.Rect.Y); - - if (structure.ResizeHorizontal) - { - if (structure.Rect.Right > center.X) - { - Vector2 moveTo = StretchPoint( - new Vector2(structureBoundsMax.X, structure.Rect.Y - structure.Rect.Height / 2), - new Vector2(center.X, structure.Rect.Y - structure.Rect.Height / 2), - xBounds, yBounds); - structureBoundsMax.X = moveTo.X; - } - if (structure.Rect.X < center.X) - { - Vector2 moveTo = StretchPoint( - new Vector2(structureBoundsMin.X, structure.Rect.Y - structure.Rect.Height / 2), - new Vector2(center.X, structure.Rect.Y - structure.Rect.Height / 2), - xBounds, yBounds); - structureBoundsMin.X = moveTo.X; - } - } - if (structure.ResizeVertical) - { - if (structure.Rect.Y > center.X) - { - Vector2 moveTo = StretchPoint( - new Vector2(structure.Rect.Center.X, structureBoundsMax.Y), - new Vector2(structure.Rect.Center.X, center.Y), - xBounds, yBounds); - structureBoundsMax.Y = moveTo.Y; - } - if (structure.Rect.Y - structure.Rect.Height < center.Y) - { - Vector2 moveTo = StretchPoint( - new Vector2(structure.Rect.Center.X, structureBoundsMin.Y), - new Vector2(structure.Rect.Center.X, center.Y), - xBounds, yBounds); - structureBoundsMin.Y = moveTo.Y; - } - } - - structure.Rect = new Rectangle( - (int)structureBoundsMin.X, - (int)structureBoundsMax.Y, - (int)(structureBoundsMax.X - structureBoundsMin.X), - (int)(structureBoundsMax.Y - structureBoundsMin.Y)); - } - } - } - - private Vector2 StretchPoint(Vector2 point, Vector2 center, Vector2 xBounds, Vector2 yBounds) - { - Vector2 diff = point - center; - if (diff.LengthSquared() < 0.0001f) return point; - - Vector2? closestIntersection = RayCastWalls(point, Vector2.Normalize(diff)); - - if (!closestIntersection.HasValue) return point; - - Vector2 moveAmount = closestIntersection.Value - point; - Vector2 moveRatio = new Vector2( - Math.Abs(diff.X) / ((xBounds.Y - xBounds.X) * 0.5f), - Math.Abs(diff.Y) / ((yBounds.Y - yBounds.X) * 0.5f)); - return point + new Vector2(moveAmount.X * moveRatio.X, moveAmount.Y * moveRatio.Y); - } - - private Vector2? RayCastWalls(Vector2 worldPosition, Vector2 dir) - { - float rayLength = 10000.0f; - Vector2 rayStart = worldPosition; - Vector2 rayEnd = worldPosition + dir * rayLength; - Vector2? closestIntersection = null; - float closestDist = rayLength * rayLength; - foreach (Line line in walls) - { - if (!MathUtils.GetLineIntersection(line.A, line.B, rayStart, rayEnd, out Vector2 intersection)) { continue; } - - intersection = line.IsHorizontal ? - new Vector2(intersection.X, intersection.Y - Math.Sign(dir.Y) * line.Radius) : - new Vector2(intersection.X - Math.Sign(dir.X) * line.Radius, intersection.Y); - - float dist = Vector2.DistanceSquared(rayStart, intersection); - if (dist < closestDist) - { - closestIntersection = intersection; - closestDist = dist; - } - } - return closestIntersection; - } - - private RuinShape FindRoom(RuinEntityConfig.RelativePlacement placement, RuinShape relativeTo) - { - switch (placement) - { - case RuinEntityConfig.RelativePlacement.SameRoom: - return relativeTo; - case RuinEntityConfig.RelativePlacement.NextRoom: - return FindNearestRoom(relativeTo, rooms, 1); - case RuinEntityConfig.RelativePlacement.NextCorridor: - return FindNearestRoom(relativeTo, corridors, 1); - case RuinEntityConfig.RelativePlacement.PreviousRoom: - return FindNearestRoom(relativeTo, rooms, -1); - case RuinEntityConfig.RelativePlacement.PreviousCorridor: - return FindNearestRoom(relativeTo, corridors, -1); - case RuinEntityConfig.RelativePlacement.FirstRoom: - return FindFirstRoom(rooms); - case RuinEntityConfig.RelativePlacement.FirstCorridor: - return FindFirstRoom(corridors); - case RuinEntityConfig.RelativePlacement.LastRoom: - return FindLastRoom(rooms); - case RuinEntityConfig.RelativePlacement.LastCorridor: - return FindLastRoom(corridors); - default: - throw new NotImplementedException(); - } - } - - /// - /// Find the nearest room relative to a specific room. - /// - /// The room to compare the distance with - /// List of rooms to check (use a list that only contains rooms/corridors if you want a specific types of rooms) - /// Direction to check: 1 = find the next room, -1 = find the previous room - private RuinShape FindNearestRoom(RuinShape relativeTo, IEnumerable roomList, int dir, Func predicate = null) - { - dir = Math.Sign(dir); - RuinShape selectedRoom = null; - foreach (RuinShape room in roomList) - { - if (room == relativeTo) continue; - if (predicate != null && !predicate(room)) continue; - int roomDir = Math.Sign(room.DistanceFromEntrance - relativeTo.DistanceFromEntrance); - - if (roomDir == 0 || roomDir == dir) - { - if (selectedRoom == null) - { - selectedRoom = room; - } - else //room already selected, check if this one is closer - { - //closer than the previously selected room - if (Math.Abs(room.DistanceFromEntrance - relativeTo.DistanceFromEntrance) < - Math.Abs(selectedRoom.DistanceFromEntrance - relativeTo.DistanceFromEntrance)) - { - selectedRoom = room; - } - //same distance measured in room indices, select the room if the actual distance is smaller - else if (room.DistanceFromEntrance == selectedRoom.DistanceFromEntrance && - Vector2.DistanceSquared(relativeTo.Center, room.Center) < Vector2.DistanceSquared(relativeTo.Center, selectedRoom.Center)) - { - selectedRoom = room; - } - } - } - } - return selectedRoom; - } - - private RuinShape FindFirstRoom(IEnumerable roomList, Func predicate = null) - { - if (!roomList.Any()) { return null; } - RuinShape firstRoom = null; - foreach (RuinShape room in roomList) - { - if (predicate != null && !predicate(room)) continue; - if (firstRoom == null || room.DistanceFromEntrance < firstRoom.DistanceFromEntrance) - { - firstRoom = room; - } - } - return firstRoom; - } - - private RuinShape FindLastRoom(IEnumerable roomList, Func predicate = null) - { - if (!roomList.Any()) { return null; } - RuinShape lastRoom = null; - foreach (RuinShape room in roomList) - { - if (predicate != null && !predicate(room)) continue; - if (lastRoom == null || room.DistanceFromEntrance > lastRoom.DistanceFromEntrance) - { - lastRoom = room; - } - } - return lastRoom; - } - } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index 4fedac730..37c1c3ab8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -1006,12 +1006,22 @@ namespace Barotrauma stockToRemove.ForEach(i => stock.Remove(i)); StoreStock = stock; - if (++StepsSinceSpecialsUpdated >= SpecialsUpdateInterval) + int extraSpecialSalesCount = GetExtraSpecialSalesCount(); + + if (++StepsSinceSpecialsUpdated >= SpecialsUpdateInterval || + DailySpecials.Count() != DailySpecialsCount + extraSpecialSalesCount) { CreateStoreSpecials(); } } + private int GetExtraSpecialSalesCount() + { + var characters = GameSession.GetSessionCrewCharacters(); + if (!characters.Any()) { return 0; } + return characters.Max(c => (int)c.GetStatValue(StatTypes.ExtraSpecialSalesCount)); + } + private void GenerateRandomPriceModifier() { StorePriceModifier = Rand.Range(-StorePriceModifierRange, StorePriceModifierRange); @@ -1035,7 +1045,9 @@ namespace Barotrauma } availableStock.Add(stockItem.ItemPrefab, weight); } - for (int i = 0; i < DailySpecialsCount; i++) + + int extraSpecialSalesCount = GetExtraSpecialSalesCount(); + for (int i = 0; i < DailySpecialsCount + extraSpecialSalesCount; i++) { if (availableStock.None()) { break; } var item = ToolBox.SelectWeightedRandom(availableStock.Keys.ToList(), availableStock.Values.ToList(), Rand.RandSync.Unsynced); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs index d2e77976e..b14ee383e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -224,12 +224,6 @@ namespace Barotrauma } } - public RuinGeneration.Ruin ParentRuin - { - get; - set; - } - [Serialize(true, true)] public bool RemoveIfLinkedOutpostDoorInUse { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs index 577750efd..7b29db634 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerationParams.cs @@ -11,7 +11,7 @@ namespace Barotrauma { public static List Params { get; private set; } - public string Name { get; private set; } + public virtual string Name { get; private set; } public string Identifier { get; private set; } @@ -67,6 +67,34 @@ namespace Barotrauma set; } + [Serialize(true, isSaveable: true), Editable] + public bool LockUnusedDoors + { + get; + set; + } + + [Serialize(true, isSaveable: true), Editable] + public bool RemoveUnusedGaps + { + get; + set; + } + + [Serialize(0.0f, isSaveable: true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] + public float MinWaterPercentage + { + get; + set; + } + + [Serialize(0.0f, isSaveable: true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)] + public float MaxWaterPercentage + { + get; + set; + } + [Serialize("", isSaveable: true), Editable] public string ReplaceInRadiation { get; set; } @@ -81,12 +109,14 @@ namespace Barotrauma public Dictionary SerializableProperties { get; private set; } - private OutpostGenerationParams(XElement element, string filePath) + protected OutpostGenerationParams(XElement element, string filePath) { Identifier = element.GetAttributeString("identifier", ""); Name = element.GetAttributeString("name", Identifier); allowedLocationTypes = element.GetAttributeStringArray("allowedlocationtypes", Array.Empty()).ToList(); SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + + if (element == null) { return; } foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs index 2e07a4c11..70e682b0b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs @@ -85,6 +85,7 @@ namespace Barotrauma var subInfo = new SubmarineInfo(outpostModuleFile.Path); if (subInfo.OutpostModuleInfo != null) { + if (subInfo.OutpostModuleInfo.ModuleFlags.Contains("ruin") != generationParams is RuinGeneration.RuinGenerationParams) { continue; } outpostModules.Add(subInfo); } } @@ -162,7 +163,7 @@ namespace Barotrauma selectedModules.Add(new PlacedModule(initialModule, null, OutpostModuleInfo.GapPosition.None)); selectedModules.Last().FulfilledModuleTypes.Add(initialModuleFlag); - AppendToModule(selectedModules.Last(), outpostModules.ToList(), pendingModuleFlags, selectedModules, locationType); + AppendToModule(selectedModules.Last(), outpostModules.ToList(), pendingModuleFlags, selectedModules, locationType, allowExtendBelowInitialModule: generationParams is RuinGeneration.RuinGenerationParams); if (pendingModuleFlags.Any(flag => !flag.Equals("none", StringComparison.OrdinalIgnoreCase))) { remainingTries--; @@ -233,17 +234,23 @@ namespace Barotrauma var selectedModule = selectedModules[i]; sub.Info.GameVersion = selectedModule.Info.GameVersion; 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) + foreach (MapEntity entity in moduleEntities.ToList()) { entity.OriginalModuleIndex = i; if (!(entity is Item item)) { continue; } - item.GetComponent()?.RefreshLinkedGap(); + var door = item.GetComponent(); + if (door != null) + { + door.RefreshLinkedGap(); + if (!moduleEntities.Contains(door.LinkedGap)) { moduleEntities.Add(door.LinkedGap); } + } item.GetComponent()?.InitializeLinks(); item.GetComponent()?.OnMapLoaded(); } + idOffset = moduleEntities.Max(e => e.ID); var wallEntities = moduleEntities.Where(e => e is Structure).Cast(); var hullEntities = moduleEntities.Where(e => e is Hull).Cast(); @@ -345,11 +352,33 @@ namespace Barotrauma Submarine.RepositionEntities(module.Offset + sub.HiddenSubPosition, entities[module]); } Gap.UpdateHulls(); - allEntities.AddRange(GenerateHallways(sub, locationType, selectedModules, outpostModules, entities)); + allEntities.AddRange(GenerateHallways(sub, locationType, selectedModules, outpostModules, entities, generationParams is RuinGeneration.RuinGenerationParams)); LinkOxygenGenerators(allEntities); - LockUnusedDoors(selectedModules, entities); + if (generationParams.LockUnusedDoors) + { + LockUnusedDoors(selectedModules, entities, generationParams.RemoveUnusedGaps); + } AlignLadders(selectedModules, entities); PowerUpOutpost(entities.SelectMany(e => e.Value)); + if (generationParams.MaxWaterPercentage > 0.0f) + { + foreach (var entity in allEntities) + { + if (entity is Hull hull) + { + float diff = generationParams.MaxWaterPercentage - generationParams.MinWaterPercentage; + if (diff < 0.01f) + { + // Overfill the hulls to get rid of air pockets in the vertical hallways. Airpockets make it impossible to swim up the hallways. + hull.WaterVolume = hull.Volume * 2; + } + else + { + hull.WaterVolume = hull.Volume * Rand.Range(generationParams.MinWaterPercentage, generationParams.MaxWaterPercentage, Rand.RandSync.Server) * 0.01f; + } + } + } + } } return allEntities; @@ -414,7 +443,8 @@ namespace Barotrauma List pendingModuleFlags, List selectedModules, LocationType locationType, - bool retry = true) + bool retry = true, + bool allowExtendBelowInitialModule = false) { if (pendingModuleFlags.Count == 0) { return true; } @@ -422,8 +452,11 @@ namespace Barotrauma foreach (OutpostModuleInfo.GapPosition gapPosition in GapPositions().Randomize(Rand.RandSync.Server)) { if (currentModule.UsedGapPositions.HasFlag(gapPosition)) { continue; } - //don't continue downwards if it'd extend below the airlock - if (gapPosition == OutpostModuleInfo.GapPosition.Bottom && currentModule.Offset.Y <= 1) { continue; } + if (!allowExtendBelowInitialModule) + { + //don't continue downwards if it'd extend below the airlock + if (gapPosition == OutpostModuleInfo.GapPosition.Bottom && currentModule.Offset.Y <= 1) { continue; } + } if (currentModule.Info.OutpostModuleInfo.GapPositions.HasFlag(gapPosition)) { var newModule = AppendModule(currentModule, GetOpposingGapPosition(gapPosition), availableModules, pendingModuleFlags, selectedModules, locationType); @@ -438,7 +471,7 @@ namespace Barotrauma //try to append to some other module first foreach (PlacedModule otherModule in selectedModules) { - if (AppendToModule(otherModule, availableModules, pendingModuleFlags, selectedModules, locationType, retry: false)) + if (AppendToModule(otherModule, availableModules, pendingModuleFlags, selectedModules, locationType, retry: false, allowExtendBelowInitialModule: allowExtendBelowInitialModule)) { return true; } @@ -454,7 +487,7 @@ namespace Barotrauma //retry currentModule = AppendModule(currentModule.PreviousModule, currentModule.ThisGapPosition, availableModules, pendingModuleFlags, selectedModules, locationType); if (currentModule == null) { break; } - if (AppendToModule(currentModule, availableModules, pendingModuleFlags, selectedModules, locationType, retry: false)) + if (AppendToModule(currentModule, availableModules, pendingModuleFlags, selectedModules, locationType, retry: false, allowExtendBelowInitialModule: allowExtendBelowInitialModule)) { return true; } @@ -676,6 +709,10 @@ namespace Barotrauma else { availableModules = modules.Where(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag)); + if (moduleFlag != "hallwayhorizontal" && moduleFlag != "hallwayvertical") + { + availableModules = availableModules.Where(m => !m.OutpostModuleInfo.ModuleFlags.Contains("hallwayhorizontal") && !m.OutpostModuleInfo.ModuleFlags.Contains("hallwayvertical")); + } } if (availableModules.Count() == 0) { return null; } @@ -840,7 +877,7 @@ namespace Barotrauma return from.AllowAttachToModules.Any(s => to.ModuleFlags.Contains(s)); } - private static List GenerateHallways(Submarine sub, LocationType locationType, IEnumerable placedModules, IEnumerable availableModules, Dictionary> allEntities) + private static List GenerateHallways(Submarine sub, LocationType locationType, IEnumerable placedModules, IEnumerable availableModules, Dictionary> allEntities, bool isRuin) { //if a hallway is shorter than this, one of the doors at the ends of the hallway is removed const float MinTwoDoorHallwayLength = 32.0f; @@ -1193,14 +1230,13 @@ namespace Barotrauma } } - private static void LockUnusedDoors(IEnumerable placedModules, Dictionary> entities) + private static void LockUnusedDoors(IEnumerable placedModules, Dictionary> entities, bool removeUnusedGaps) { foreach (PlacedModule module in placedModules) { foreach (MapEntity me in entities[module]) { - var gap = me as Gap; - if (gap == null) { continue; } + if (!(me is Gap gap)) { continue; } var door = gap.ConnectedDoor; if (door != null && !door.UseBetweenOutpostModules) { continue; } if (placedModules.Any(m => m.PreviousGap == gap || m.ThisGap == gap)) @@ -1247,11 +1283,11 @@ namespace Barotrauma if (connectionPanel != null) { connectionPanel.Locked = true; } } } - else + else if (removeUnusedGaps) { gap.Remove(); WayPoint.WayPointList.Where(wp => wp.ConnectedGap == gap).ForEachMod(wp => wp.Remove()); - } + } } entities[module].RemoveAll(e => e.Removed); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs index 4b0307e7e..c0c051586 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostModuleInfo.cs @@ -89,11 +89,13 @@ namespace Barotrauma if (newFlags.Contains("hallwayhorizontal")) { moduleFlags.Add("hallwayhorizontal"); + if (newFlags.Contains("ruin")) { moduleFlags.Add("ruin"); } return; } if (newFlags.Contains("hallwayvertical")) { moduleFlags.Add("hallwayvertical"); + if (newFlags.Contains("ruin")) { moduleFlags.Add("ruin"); } return; } if (!newFlags.Any()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs index e97b6fa06..d8dc6665f 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, BeaconStation, EnemySubmarine } + public enum SubmarineType { Player, Outpost, OutpostModule, Wreck, BeaconStation, EnemySubmarine, Ruin } public enum SubmarineClass { Undefined, Scout, Attack, Transport, DeepDiver } partial class SubmarineInfo : IDisposable @@ -97,11 +97,10 @@ namespace Barotrauma public bool IsOutpost => Type == SubmarineType.Outpost || Type == SubmarineType.OutpostModule; - //TODO: replace when the ruin branch is merged - public bool IsRuin => false; public bool IsWreck => Type == SubmarineType.Wreck; public bool IsBeacon => Type == SubmarineType.BeaconStation; public bool IsPlayer => Type == SubmarineType.Player; + public bool IsRuin => Type == SubmarineType.Ruin; public bool IsCampaignCompatible => IsPlayer && !HasTag(SubmarineTag.Shuttle) && !HasTag(SubmarineTag.HideInMenus) && SubmarineClass != SubmarineClass.Undefined; public bool IsCampaignCompatibleIgnoreClass => IsPlayer && !HasTag(SubmarineTag.Shuttle) && !HasTag(SubmarineTag.HideInMenus); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs index e6942d2bb..3325ea591 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; -using Barotrauma.RuinGeneration; using Barotrauma.Extensions; namespace Barotrauma @@ -189,61 +188,141 @@ namespace Barotrauma door.Body.Enabled = true; } } - + bool isFlooded = submarine.Info.IsRuin || submarine.Info.Type == SubmarineType.OutpostModule && submarine.Info.OutpostModuleInfo.ModuleFlags.Contains("ruin"); float diffFromHullEdge = 50; float minDist = 100.0f; float heightFromFloor = 110.0f; float hullMinHeight = 100; + var removals = new List(); foreach (Hull hull in Hull.hullList) { - // Ignore hulls that a human couldn't fit in. - // Doesn't take multi-hull rooms into account, but it's probably best to leave them to be setup manually. - if (hull.Rect.Height < hullMinHeight) { continue; } - // Do five raycasts to check if there's a floor. Don't create waypoints unless we can find a floor. - Body floor = null; - for (int i = 0; i < 5; i++) + if (isFlooded) { - float horizontalOffset = 0; - switch (i) + diffFromHullEdge = 75; + var hullWaypoints = new List(); + float top = hull.Rect.Y; + float bottom = hull.Rect.Y - hull.Rect.Height; + if (hull.Rect.Width < 300 || hull.Rect.Height < 300) { - case 1: - horizontalOffset = hull.RectWidth * 0.2f; - break; - case 2: - horizontalOffset = hull.RectWidth * 0.4f; - break; - case 3: - horizontalOffset = -hull.RectWidth * 0.2f; - break; - case 4: - horizontalOffset = -hull.RectWidth * 0.4f; - break; + // For narrow hulls, create one line of waypoints either horizontally or vertically + if (hull.Rect.Width > hull.Rect.Height) + { + // Horizontal + float y = hull.Rect.Y - hull.Rect.Height / 2; + for (float x = hull.Rect.X + diffFromHullEdge; x <= hull.Rect.Right - diffFromHullEdge; x += minDist) + { + hullWaypoints.Add(new WayPoint(new Vector2(x, y), SpawnType.Path, submarine)); + } + } + else + { + // Vertical + float x = hull.Rect.X + hull.Rect.Width / 2; + for (float y = top - diffFromHullEdge; y >= bottom + diffFromHullEdge; y -= minDist) + { + hullWaypoints.Add(new WayPoint(new Vector2(x, y), SpawnType.Path, submarine)); + } + } + } + if (hullWaypoints.None()) + { + // Try to create a grid-like network of waypoints + for (float x = hull.Rect.X + diffFromHullEdge; x <= hull.Rect.Right - diffFromHullEdge; x += minDist) + { + for (float y = top - diffFromHullEdge; y >= bottom + diffFromHullEdge; y -= minDist) + { + hullWaypoints.Add(new WayPoint(new Vector2(x, y), SpawnType.Path, submarine)); + } + } + if (hullWaypoints.None()) + { + // If that fails, just create one waypoint at the center. + hullWaypoints.Add(new WayPoint(new Vector2(hull.Rect.X + hull.Rect.Width / 2.0f, hull.Rect.Y - hull.Rect.Height / 2), SpawnType.Path, submarine)); + } + foreach (WayPoint wp in hullWaypoints) + { + foreach (Structure wall in Structure.WallList) + { + if (wall.HasBody) + { + // Remove waypoints that are too close/inside the walls. + Rectangle rect = wall.Rect; + rect.Inflate(10, 10); + if (rect.ContainsWorld(wp.Position)) + { + removals.Add(wp); + } + } + } + } + } + // Connect the waypoints + foreach (var wayPoint in hullWaypoints) + { + for (int dir = -1; dir <= 1; dir += 2) + { + WayPoint closest = wayPoint.FindClosest(dir, horizontalSearch: true, new Vector2(minDist * 1.9f, minDist)); + if (closest != null && closest.CurrentHull == wayPoint.CurrentHull) + { + wayPoint.ConnectTo(closest); + } + closest = wayPoint.FindClosest(dir, horizontalSearch: false, new Vector2(minDist, minDist * 1.9f)); + if (closest != null && closest.CurrentHull == wayPoint.CurrentHull) + { + wayPoint.ConnectTo(closest); + } + } } - horizontalOffset = ConvertUnits.ToSimUnits(horizontalOffset); - Vector2 floorPos = new Vector2(hull.SimPosition.X + horizontalOffset, ConvertUnits.ToSimUnits(hull.Rect.Y - hull.RectHeight - 50)); - floor = Submarine.PickBody(new Vector2(hull.SimPosition.X + horizontalOffset, hull.SimPosition.Y), floorPos, collisionCategory: Physics.CollisionWall | Physics.CollisionPlatform, customPredicate: f => !(f.Body.UserData is Submarine)); - if (floor != null) { break; } - } - if (floor == null) { continue; } - float waypointHeight = hull.Rect.Height > heightFromFloor * 2 ? heightFromFloor : hull.Rect.Height / 2; - if (hull.Rect.Width < diffFromHullEdge * 3.0f) - { - new WayPoint(new Vector2(hull.Rect.X + hull.Rect.Width / 2.0f, hull.Rect.Y - hull.Rect.Height + waypointHeight), SpawnType.Path, submarine); } else { - WayPoint prevWaypoint = null; - for (float x = hull.Rect.X + diffFromHullEdge; x <= hull.Rect.Right - diffFromHullEdge; x += minDist) + if (hull.Rect.Height < hullMinHeight) { continue; } + // Do five raycasts to check if there's a floor. Don't create waypoints unless we can find a floor. + Body floor = null; + for (int i = 0; i < 5; i++) { - var wayPoint = new WayPoint(new Vector2(x, hull.Rect.Y - hull.Rect.Height + waypointHeight), SpawnType.Path, submarine); - if (prevWaypoint != null) { wayPoint.ConnectTo(prevWaypoint); } - prevWaypoint = wayPoint; + float horizontalOffset = 0; + switch (i) + { + case 1: + horizontalOffset = hull.RectWidth * 0.2f; + break; + case 2: + horizontalOffset = hull.RectWidth * 0.4f; + break; + case 3: + horizontalOffset = -hull.RectWidth * 0.2f; + break; + case 4: + horizontalOffset = -hull.RectWidth * 0.4f; + break; + } + horizontalOffset = ConvertUnits.ToSimUnits(horizontalOffset); + Vector2 floorPos = new Vector2(hull.SimPosition.X + horizontalOffset, ConvertUnits.ToSimUnits(hull.Rect.Y - hull.RectHeight - 50)); + floor = Submarine.PickBody(new Vector2(hull.SimPosition.X + horizontalOffset, hull.SimPosition.Y), floorPos, collisionCategory: Physics.CollisionWall | Physics.CollisionPlatform, customPredicate: f => !(f.Body.UserData is Submarine)); + if (floor != null) { break; } } - if (prevWaypoint == null) + if (floor == null) { continue; } + float waypointHeight = hull.Rect.Height > heightFromFloor * 2 ? heightFromFloor : hull.Rect.Height / 2; + if (hull.Rect.Width < diffFromHullEdge * 3.0f) { - // Ensure that we always create at least one waypoint per hull. new WayPoint(new Vector2(hull.Rect.X + hull.Rect.Width / 2.0f, hull.Rect.Y - hull.Rect.Height + waypointHeight), SpawnType.Path, submarine); } + else + { + WayPoint previousWaypoint = null; + for (float x = hull.Rect.X + diffFromHullEdge; x <= hull.Rect.Right - diffFromHullEdge; x += minDist) + { + var wayPoint = new WayPoint(new Vector2(x, hull.Rect.Y - hull.Rect.Height + waypointHeight), SpawnType.Path, submarine); + if (previousWaypoint != null) { wayPoint.ConnectTo(previousWaypoint); } + previousWaypoint = wayPoint; + } + if (previousWaypoint == null) + { + // Ensure that we always create at least one waypoint per hull. + new WayPoint(new Vector2(hull.Rect.X + hull.Rect.Width / 2.0f, hull.Rect.Y - hull.Rect.Height + waypointHeight), SpawnType.Path, submarine); + } + } } } @@ -278,7 +357,7 @@ namespace Barotrauma } float outSideWaypointInterval = 100.0f; - if (submarine.Info.Type != SubmarineType.OutpostModule) + if (!isFlooded && submarine.Info.Type != SubmarineType.OutpostModule) { List<(WayPoint, int)> outsideWaypoints = new List<(WayPoint, int)>(); @@ -381,7 +460,6 @@ namespace Barotrauma } } // Remove unwanted points - var removals = new List(); WayPoint previous = null; float tooClose = outSideWaypointInterval / 2; foreach (var wayPoint in outsideWaypoints) @@ -412,7 +490,6 @@ namespace Barotrauma foreach (WayPoint wp in removals) { outsideWaypoints.RemoveAll(w => w.Item1 == wp); - wp.Remove(); } for (int i = 0; i < outsideWaypoints.Count; i++) { @@ -433,41 +510,35 @@ namespace Barotrauma } } } - - List stairList = new List(); - foreach (MapEntity me in mapEntityList) - { - if (!(me is Structure stairs)) { continue; } - - if (stairs.StairDirection != Direction.None) stairList.Add(stairs); - } - - foreach (Structure stairs in stairList) + foreach (Structure wall in Structure.WallList) { + if (wall.StairDirection == Direction.None) { continue; } WayPoint[] stairPoints = new WayPoint[3]; stairPoints[0] = new WayPoint( - new Vector2(stairs.Rect.X - 32.0f, - stairs.Rect.Y - (stairs.StairDirection == Direction.Left ? 80 : stairs.Rect.Height) + heightFromFloor), SpawnType.Path, submarine); + new Vector2(wall.Rect.X - 32.0f, + wall.Rect.Y - (wall.StairDirection == Direction.Left ? 80 : wall.Rect.Height) + heightFromFloor), SpawnType.Path, submarine); stairPoints[1] = new WayPoint( - new Vector2(stairs.Rect.Right + 32.0f, - stairs.Rect.Y - (stairs.StairDirection == Direction.Left ? stairs.Rect.Height : 80) + heightFromFloor), SpawnType.Path, submarine); + new Vector2(wall.Rect.Right + 32.0f, + wall.Rect.Y - (wall.StairDirection == Direction.Left ? wall.Rect.Height : 80) + heightFromFloor), SpawnType.Path, submarine); - for (int i = 0; i < 2; i++ ) + for (int i = 0; i < 2; i++) { for (int dir = -1; dir <= 1; dir += 2) { WayPoint closest = stairPoints[i].FindClosest(dir, horizontalSearch: true, new Vector2(100, 70)); if (closest == null) { continue; } stairPoints[i].ConnectTo(closest); - } + } } - + stairPoints[2] = new WayPoint((stairPoints[0].Position + stairPoints[1].Position) / 2, SpawnType.Path, submarine); stairPoints[0].ConnectTo(stairPoints[2]); stairPoints[2].ConnectTo(stairPoints[1]); } + removals.ForEach(wp => wp.Remove()); + removals.Clear(); foreach (Item item in Item.ItemList) { @@ -605,12 +676,25 @@ namespace Barotrauma { if (gap.IsHorizontal) { - // Too small to walk through - if (gap.Rect.Height < hullMinHeight) { continue; } + if ( isFlooded) + { + // Too small to swim through + if (gap.Rect.Height < 50) { continue; } + } + else + { + // Too small to walk through + if (gap.Rect.Height < hullMinHeight) { continue; } + } + Vector2 pos = new Vector2(gap.Rect.Center.X, gap.Rect.Y - gap.Rect.Height + heightFromFloor); + if (isFlooded) + { + pos.Y = gap.Rect.Y - gap.Rect.Height / 2; + } var wayPoint = new WayPoint(pos, SpawnType.Path, submarine, gap); // The closest waypoint can be quite far if the gap is at an exterior door. - Vector2 tolerance = gap.IsRoomToRoom ? new Vector2(150, 70) : new Vector2(1000, 1000); + Vector2 tolerance = gap.IsRoomToRoom && !isFlooded ? new Vector2(150, 70) : new Vector2(1000, 1000); for (int dir = -1; dir <= 1; dir += 2) { WayPoint closest = wayPoint.FindClosest(dir, horizontalSearch: true, tolerance, gap.ConnectedDoor?.Body.FarseerBody); @@ -623,7 +707,7 @@ namespace Barotrauma else { // Create waypoints on vertical gaps on the outer walls, also hatches. - if (gap.IsRoomToRoom || gap.linkedTo.None(l => l is Hull)) { continue; } + if (!isFlooded && (gap.IsRoomToRoom || gap.linkedTo.None(l => l is Hull))) { continue; } // Too small to swim through if (gap.Rect.Width < 50.0f) { continue; } Vector2 pos = new Vector2(gap.Rect.Center.X, gap.Rect.Y - gap.Rect.Height / 2); @@ -632,11 +716,20 @@ namespace Barotrauma var wayPoint = new WayPoint(pos, SpawnType.Path, submarine, gap); Hull connectedHull = (Hull)gap.linkedTo.First(l => l is Hull); int dir = Math.Sign(connectedHull.Position.Y - gap.Position.Y); - WayPoint closest = wayPoint.FindClosest(dir, horizontalSearch: false, new Vector2(50, 100)); + WayPoint closest = wayPoint.FindClosest(dir, horizontalSearch: false, isFlooded ? new Vector2(500, 500) : new Vector2(50, 100)); if (closest != null) { wayPoint.ConnectTo(closest); } + if (isFlooded) + { + closest = wayPoint.FindClosest(-dir, horizontalSearch: false, isFlooded ? new Vector2(500, 500) : new Vector2(50, 100)); + if (closest != null) + { + wayPoint.ConnectTo(closest); + } + } + // Link to outside for (dir = -1; dir <= 1; dir += 2) { closest = wayPoint.FindClosest(dir, horizontalSearch: true, new Vector2(500, 1000), gap.ConnectedDoor?.Body.FarseerBody, filter: wp => wp.CurrentHull == null); @@ -656,7 +749,7 @@ namespace Barotrauma foreach (WayPoint wp in WayPointList) { - if (wp.CurrentHull == null && wp.Ladders == null && wp.linkedTo.Count < 2) + if (wp.SpawnType == SpawnType.Path && wp.CurrentHull == null && wp.Ladders == null && wp.linkedTo.Count < 2) { DebugConsole.ThrowError($"Couldn't automatically link the waypoint {wp.ID} outside of the submarine. You should do it manually. The waypoint ID is shown in red color."); } @@ -775,11 +868,10 @@ namespace Barotrauma } } - public static WayPoint GetRandom(SpawnType spawnType = SpawnType.Human, JobPrefab assignedJob = null, Submarine sub = null, Ruin ruin = null, bool useSyncedRand = false) + public static WayPoint GetRandom(SpawnType spawnType = SpawnType.Human, JobPrefab assignedJob = null, Submarine sub = null, bool useSyncedRand = false) { return WayPointList.GetRandom(wp => wp.Submarine == sub && - wp.ParentRuin == ruin && wp.spawnType == spawnType && (assignedJob == null || (assignedJob != null && wp.AssignedJob == assignedJob)), useSyncedRand ? Rand.RandSync.Server : Rand.RandSync.Unsynced); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs index 3d6b8ea44..a4f18118c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs @@ -1,4 +1,5 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; @@ -28,6 +29,7 @@ namespace Barotrauma public bool SpawnIfInventoryFull = true; public bool IgnoreLimbSlots = false; + public InvSlotType Slot = InvSlotType.None; private readonly Action onSpawned; @@ -73,7 +75,8 @@ namespace Barotrauma { Condition = Condition }; - if (!Inventory.Owner.Removed && !Inventory.TryPutItem(spawnedItem, null, spawnedItem.AllowedSlots)) + var slot = Slot != InvSlotType.None ? Slot.ToEnumerable() : spawnedItem.AllowedSlots; + if (!Inventory.Owner.Removed && !Inventory.TryPutItem(spawnedItem, null, slot)) { if (IgnoreLimbSlots) { @@ -264,7 +267,7 @@ namespace Barotrauma spawnQueue.Enqueue(new ItemSpawnInfo(itemPrefab, position, sub, onSpawned, condition)); } - public void AddToSpawnQueue(ItemPrefab itemPrefab, Inventory inventory, float? condition = null, Action onSpawned = null, bool spawnIfInventoryFull = true, bool ignoreLimbSlots = false) + public void AddToSpawnQueue(ItemPrefab itemPrefab, Inventory inventory, float? condition = null, Action onSpawned = null, bool spawnIfInventoryFull = true, bool ignoreLimbSlots = false, InvSlotType slot = InvSlotType.None) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } if (itemPrefab == null) @@ -277,7 +280,8 @@ namespace Barotrauma spawnQueue.Enqueue(new ItemSpawnInfo(itemPrefab, inventory, onSpawned, condition) { SpawnIfInventoryFull = spawnIfInventoryFull, - IgnoreLimbSlots = ignoreLimbSlots + IgnoreLimbSlots = ignoreLimbSlots, + Slot = slot }); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs index d09effa0e..85868e116 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs @@ -756,12 +756,13 @@ namespace Barotrauma Vector2 vel = FarseerBody.LinearVelocity; Vector2 deltaPos = simPosition - (Vector2)pullPos; -#if DEBUG if (deltaPos.LengthSquared() > 100.0f * 100.0f) { +#if DEBUG DebugConsole.ThrowError("Attempted to move a physics body to an invalid position.\n" + Environment.StackTrace.CleanupStackTrace()); - } #endif + return; + } deltaPos *= force; ApplyLinearImpulse((deltaPos - vel * 0.5f) * FarseerBody.Mass, (Vector2)pullPos); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs index c3e93bdd9..cb7f26141 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs @@ -146,6 +146,7 @@ namespace Barotrauma { typeof(Vector4), "vector4" }, { typeof(Rectangle), "rectangle" }, { typeof(Color), "color" }, + { typeof(string[]), "stringarray" } }; private static readonly Dictionary> cachedProperties = @@ -273,6 +274,9 @@ namespace Barotrauma case "rectangle": PropertyInfo.SetValue(parentObject, XMLExtensions.ParseRect(value, true)); break; + case "stringarray": + PropertyInfo.SetValue(parentObject, XMLExtensions.ParseStringArray(value)); + break; } } @@ -345,6 +349,9 @@ namespace Barotrauma case "rectangle": PropertyInfo.SetValue(parentObject, XMLExtensions.ParseRect((string)value, false)); return true; + case "stringarray": + PropertyInfo.SetValue(parentObject, XMLExtensions.ParseStringArray((string)value)); + break; default: DebugConsole.ThrowError("Failed to set the value of the property \"" + Name + "\" of \"" + parentObject.ToString() + "\" to " + value.ToString()); DebugConsole.ThrowError("(Cannot convert a string to a " + PropertyType.ToString() + ")"); @@ -723,6 +730,10 @@ namespace Barotrauma case "rectangle": stringValue = XMLExtensions.RectToString((Rectangle)value); break; + case "stringarray": + string[] stringArray = (string[])value; + stringValue = stringArray != null ? string.Join(';', stringArray) : ""; + break; default: stringValue = value.ToString(); break; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index 21fa03573..780aaa767 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs @@ -520,10 +520,12 @@ namespace Barotrauma vector.W.ToString(format, CultureInfo.InvariantCulture); } + [Obsolete("Prefer XMLExtensions.ToStringHex")] public static string ColorToString(Color color) - { - return color.R + "," + color.G + "," + color.B + "," + color.A; - } + => $"{color.R},{color.G},{color.B},{color.A}"; + + public static string ToStringHex(this Color color) + => $"#{color.R:X2}{color.G:X2}{color.B:X2}{color.A:X2}"; public static string RectToString(Rectangle rect) { @@ -718,6 +720,11 @@ namespace Barotrauma return floatArray; } + public static string[] ParseStringArray(string stringArrayValues) + { + return string.IsNullOrEmpty(stringArrayValues) ? new string[0] : stringArrayValues.Split(';'); + } + public static bool IsOverride(this XElement element) => element.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase); public static bool IsCharacterVariant(this XElement element) => element.Name.ToString().Equals("charactervariant", StringComparison.OrdinalIgnoreCase); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs b/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs index 40d2a1742..db53a6db6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs @@ -151,6 +151,12 @@ namespace Barotrauma { sourceVector = overrideElement.GetAttributeVector4("sourcerect", Vector4.Zero); } + if ((overrideElement ?? SourceElement).Attribute("sheetindex") != null) + { + Point sheetElementSize = (overrideElement ?? SourceElement).GetAttributePoint("sheetelementsize", Point.Zero); + Point sheetIndex = (overrideElement ?? SourceElement).GetAttributePoint("sheetindex", Point.Zero); + sourceVector = new Vector4(sheetIndex.X * sheetElementSize.X, sheetIndex.Y * sheetElementSize.Y, sheetElementSize.X, sheetElementSize.Y); + } Compress = SourceElement.GetAttributeBool("compress", true); bool shouldReturn = false; if (!lazyLoad) @@ -294,6 +300,12 @@ namespace Barotrauma { sourceRect = overrideElement.GetAttributeRect("sourcerect", Rectangle.Empty); } + if ((overrideElement ?? SourceElement).Attribute("sheetindex") != null) + { + Point sheetElementSize = (overrideElement ?? SourceElement).GetAttributePoint("sheetelementsize", Point.Zero); + Point sheetIndex = (overrideElement ?? SourceElement).GetAttributePoint("sheetindex", Point.Zero); + sourceRect = new Rectangle(sheetIndex.X * sheetElementSize.X, sheetIndex.Y * sheetElementSize.Y, sheetElementSize.X, sheetElementSize.Y); + } size = SourceElement.GetAttributeVector2("size", Vector2.One); size.X *= sourceRect.Width; size.Y *= sourceRect.Height; diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index d5a517d5a..d0f70222e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -144,6 +144,7 @@ namespace Barotrauma public readonly float Spread; public readonly SpawnRotationType RotationType; public readonly float AimSpread; + public readonly bool Equip; public ItemSpawnInfo(XElement element, string parentDebugName) { @@ -181,6 +182,7 @@ namespace Barotrauma Count = element.GetAttributeInt("count", 1); Spread = element.GetAttributeFloat("spread", 0f); AimSpread = element.GetAttributeFloat("aimspread", 0f); + Equip = element.GetAttributeBool("equip", false); string spawnTypeStr = element.GetAttributeString("spawnposition", "This"); if (!Enum.TryParse(spawnTypeStr, ignoreCase: true, out SpawnPosition)) @@ -308,13 +310,15 @@ namespace Barotrauma /// private readonly HashSet<(string affliction, float strength)> requiredAfflictions; + public float AfflictionMultiplier = 1.0f; + public List Afflictions { get; private set; } - private bool modifyAfflictionsByMaxVitality; + private readonly bool modifyAfflictionsByMaxVitality; public IEnumerable SpawnCharacters { @@ -571,11 +575,14 @@ namespace Barotrauma requiredItems.Add(newRequiredItem); break; case "requiredaffliction": - requiredAfflictions ??= new HashSet<(string, float)>(); - requiredAfflictions.Add(( - subElement.GetAttributeString("identifier", string.Empty), - subElement.GetAttributeFloat("minstrength", 0.0f))); + string[] ids = subElement.GetAttributeStringArray("identifier", new string[0]); + foreach (string afflictionId in ids) + { + requiredAfflictions.Add(( + afflictionId, + subElement.GetAttributeFloat("minstrength", 0.0f))); + } break; case "conditional": foreach (XAttribute attribute in subElement.Attributes()) @@ -798,7 +805,16 @@ namespace Barotrauma { var target = targets.FirstOrDefault(t => t is Item || t is ItemComponent); var targetItem = target as Item ?? (target as ItemComponent)?.Item; - if (targetItem?.ParentInventory == null) { continue; } + if (targetItem?.ParentInventory == null) + { + //if we're checking for inequality, not being inside a valid container counts as success + //(not inside a container = the container doesn't have a specific tag/value) + if (pc.Operator == PropertyConditional.OperatorType.NotEquals) + { + return true; + } + continue; + } var owner = targetItem.ParentInventory.Owner; if (pc.TargetGrandParent && owner is Item ownerItem) { @@ -816,7 +832,7 @@ namespace Barotrauma if (HasRequiredConditions(container.AllPropertyObjects, pc.ToEnumerable(), targetingContainer: true)) { return true; } } } - if (owner is Character character && HasRequiredConditions(character.ToEnumerable(), pc.ToEnumerable(), targetingContainer: true)) { return true; } + if (owner is Character character && HasRequiredConditions(character.ToEnumerable(), pc.ToEnumerable(), targetingContainer: true)) { return true; } } else { @@ -841,7 +857,16 @@ namespace Barotrauma { var target = targets.FirstOrDefault(t => t is Item || t is ItemComponent); var targetItem = target as Item ?? (target as ItemComponent)?.Item; - if (targetItem?.ParentInventory == null) { return false; } + if (targetItem?.ParentInventory == null) + { + //if we're checking for inequality, not being inside a valid container counts as success + //(not inside a container = the container doesn't have a specific tag/value) + if (pc.Operator == PropertyConditional.OperatorType.NotEquals) + { + continue; + } + return false; + } var owner = targetItem.ParentInventory.Owner; if (pc.TargetGrandParent && owner is Item ownerItem) { @@ -1424,7 +1449,19 @@ namespace Barotrauma } if (inventory != null && (inventory.CanBePut(chosenItemSpawnInfo.ItemPrefab) || chosenItemSpawnInfo.SpawnIfInventoryFull)) { - Entity.Spawner.AddToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, inventory, spawnIfInventoryFull: chosenItemSpawnInfo.SpawnIfInventoryFull); + Entity.Spawner.AddToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, inventory, spawnIfInventoryFull: chosenItemSpawnInfo.SpawnIfInventoryFull, onSpawned: item => + { + if (chosenItemSpawnInfo.Equip && entity is Character character && character.Inventory != null) + { + //if the item is both pickable and wearable, try to wear it instead of picking it up + List allowedSlots = + item.GetComponents().Count() > 1 ? + new List(item.GetComponent()?.AllowedSlots ?? item.GetComponent().AllowedSlots) : + new List(item.AllowedSlots); + allowedSlots.Remove(InvSlotType.Any); + character.Inventory.TryPutItem(item, null, allowedSlots); + } + }); } } break; @@ -1616,7 +1653,7 @@ namespace Barotrauma { multiplier *= 1 + targetCharacter.GetStatValue(StatTypes.MedicalItemEffectivenessMultiplier); } - return multiplier; + return multiplier * AfflictionMultiplier; } private Affliction GetMultipliedAffliction(Affliction affliction, Entity entity, Character targetCharacter, float deltaTime, bool modifyByMaxVitality) @@ -1636,11 +1673,11 @@ namespace Barotrauma private void RegisterTreatmentResults(Entity entity, Limb limb, Affliction affliction, AttackResult result) { - if (entity is Item item && item.UseInHealthInterface) + if (entity is Item item && item.UseInHealthInterface && limb != null) { foreach (Affliction limbAffliction in limb.character.CharacterHealth.GetAllAfflictions()) { - if (result.Afflictions.Any(a => a.Prefab == limbAffliction.Prefab) && + if (result.Afflictions != null && result.Afflictions.Any(a => a.Prefab == limbAffliction.Prefab) && (!affliction.Prefab.LimbSpecific || limb.character.CharacterHealth.GetAfflictionLimb(affliction) == limb)) { if (type == ActionType.OnUse) diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index b84699444..56e627677 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,39 @@ +--------------------------------------------------------------------------------------------------------- +v0.1500.5.0 +--------------------------------------------------------------------------------------------------------- + +Additions and changes: +- Overhauled ruins: completely remade sprites, monsters, layouts, items and puzzles. +- New Scan mission: scan an Alien ruin by placing down provided scanners at target locations and take the scanners back to the outpost. +- New Alien Ruin mission: kill the guardians inhabiting the ruin and destroy their pods. +- Improvements and fixes to the overhauled character sprites and animations. +- More talent improvements and additions. +- Balanced loot spawn rates. +- Cap the amount argument of the spawnitem command to 100 to prevent freezing/crashing when trying to spawn a ridiculous amount of the item. +- Nerfed concussions: they now require a larger amount of damage to the head to trigger and slowly heal by themselves. +- Added "unlocktalents [job]" command. +- Don't reset the selected limb when reopening the health interface. +- Bots no longer ignore unconscious targets that regenerate health (i.e. they will finish off downed husks to prevent them from getting back up again). +- The health interface displays a prediction of how much a medical item will reduce/increase the afflictions. +- Certain afflictions make the characters' face change color a bit. +- Added button to randomize character appearance in the character customization menus. +- Permanently reduce character skills when respawning mid-round. The talent system makes it easier to gain skills and permanent improvements to the character, and this change is intended to balance that out. + +Fixes: +- Fixed crash when firing a syring gun (unstable only). +- Fixed crashing when using meds in multiplayer with friendly fire turned off (unstable only). +- Fixed inability to repair with hardened/dementonite wrenches (unstable only). +- Fixed item quality not persisting between rounds (unstable only). +- Fixed fabricator not outputting high-quality items in full condition if the item's quality modifiers increase the max condition (unstable). +- Display the max condition of the required item on the fabricator (i.e. show that depleted fuel needs to be fabricated from depleted fuel rods). +- Fixed holdable components that block players (e.g. mudraptor shells) causing a "you are removing a body that is not in the simulation" exception when ending a round (unstable only). +- Fixed handheld status monitor and electrical monitor UIs popping up when picking up the item. +- Fixed tracer particles not starting from the position of ranged weapons' barrel. +- Fixed inability to open the pause menu when the cursor is over an inventory slot. +- Fixed handcuffs dropping off from characters' hands when they die or turn into a husk. +- Fixed ability to crouch on ladders (unstable only). +- Fixed loadsub command. + --------------------------------------------------------------------------------------------------------- v0.1500.4.0 --------------------------------------------------------------------------------------------------------- diff --git a/Libraries/Farseer Physics Engine 3.5/Dynamics/World.cs b/Libraries/Farseer Physics Engine 3.5/Dynamics/World.cs index 8b1ddfa8d..e8b8cbea2 100644 --- a/Libraries/Farseer Physics Engine 3.5/Dynamics/World.cs +++ b/Libraries/Farseer Physics Engine 3.5/Dynamics/World.cs @@ -1042,8 +1042,6 @@ namespace FarseerPhysics.Dynamics body._world = null; BodyList.Remove(body); - body.UserData = null; - if (BodyRemoved != null) BodyRemoved(this, body);