From 3043a9a7bc61227162584c48da42b3eb7e0210a3 Mon Sep 17 00:00:00 2001 From: Markus Isberg <3e849f2e5c@pm.me> Date: Thu, 23 Sep 2021 21:29:31 +0900 Subject: [PATCH] Unstable 0.1500.4.0 (Shrek edition) --- .../ClientSource/Characters/CharacterInfo.cs | 484 ++++++++++- .../Characters/Health/CharacterHealth.cs | 105 ++- .../ClientSource/Characters/Limb.cs | 165 +++- .../Events/Missions/MineralMission.cs | 10 +- .../BarotraumaClient/ClientSource/GUI/GUI.cs | 19 +- .../ClientSource/GUI/GUIDropDown.cs | 6 +- .../ClientSource/GUI/GUIStyle.cs | 2 +- .../ClientSource/GUI/TabMenu.cs | 167 +++- .../BarotraumaClient/ClientSource/GameMain.cs | 1 - .../GameModes/MultiPlayerCampaign.cs | 2 +- .../ClientSource/GameSession/GameSession.cs | 60 +- .../ClientSource/Items/CharacterInventory.cs | 2 +- .../Items/Components/GeneticMaterial.cs | 4 +- .../Components/Machines/Deconstructor.cs | 2 +- .../Items/Components/Machines/Fabricator.cs | 6 +- .../Items/Components/Machines/MiniMap.cs | 65 +- .../ClientSource/Items/Components/Quality.cs | 21 + .../Items/Components/Repairable.cs | 9 +- .../ClientSource/Items/Inventory.cs | 9 +- .../ClientSource/Map/Map/Map.cs | 4 +- .../ClientSource/Networking/GameClient.cs | 3 + .../CampaignSetupUI/CampaignSetupUI.cs | 52 ++ .../MultiPlayerCampaignSetupUI.cs | 521 ++++++++++++ .../SinglePlayerCampaignSetupUI.cs} | 757 ++++++++---------- .../CharacterEditor/CharacterEditorScreen.cs | 47 +- .../Screens/CharacterEditor/Wizard.cs | 14 +- .../ClientSource/Screens/GameScreen.cs | 26 +- .../ClientSource/Screens/MainMenuScreen.cs | 49 +- .../ClientSource/Screens/NetLobbyScreen.cs | 555 ++++--------- .../ClientSource/Sprite/Sprite.cs | 12 +- .../Content/Effects/thresholdtint.xnb | Bin 0 -> 1501 bytes .../Content/Effects/thresholdtint_opengl.xnb | Bin 0 -> 1394 bytes .../BarotraumaClient/LinuxClient.csproj | 2 +- Barotrauma/BarotraumaClient/MacClient.csproj | 2 +- .../BarotraumaClient/Shaders/Content.mgcb | 6 + .../Shaders/Content_opengl.mgcb | 6 + .../BarotraumaClient/Shaders/thresholdtint.fx | 32 + .../Shaders/thresholdtint_opengl.fx | 32 + .../BarotraumaClient/WindowsClient.csproj | 2 +- .../BarotraumaServer/LinuxServer.csproj | 2 +- Barotrauma/BarotraumaServer/MacServer.csproj | 2 +- .../ServerSource/Characters/CharacterInfo.cs | 11 +- .../Events/Missions/MineralMission.cs | 6 +- .../Items/Components/Repairable.cs | 1 + .../ServerSource/Networking/GameServer.cs | 9 +- .../BarotraumaServer/WindowsServer.csproj | 2 +- .../Data/ContentPackages/Vanilla 0.9.xml | 1 + .../AI/Objectives/AIObjectiveManager.cs | 2 +- .../AI/Objectives/AIObjectiveRescue.cs | 2 +- .../AI/Objectives/AIObjectiveRescueAll.cs | 2 +- .../AI/Objectives/AIObjectiveReturn.cs | 8 +- .../SharedSource/Characters/AI/PathFinder.cs | 67 +- .../Characters/Animation/AnimController.cs | 28 +- .../Animation/FishAnimController.cs | 6 +- .../Animation/HumanoidAnimController.cs | 182 +++-- .../SharedSource/Characters/Character.cs | 16 +- .../SharedSource/Characters/CharacterInfo.cs | 357 +++++++-- .../Health/Afflictions/AfflictionPrefab.cs | 14 + .../Characters/Health/CharacterHealth.cs | 4 +- .../SharedSource/Characters/Jobs/JobPrefab.cs | 3 +- .../SharedSource/Characters/Limb.cs | 2 +- .../Params/Animation/AnimationParams.cs | 18 +- .../Params/Animation/FishAnimations.cs | 20 - .../Params/Animation/HumanoidAnimations.cs | 82 +- .../Params/Ragdoll/RagdollParams.cs | 10 +- .../AbilityConditionals/AbilityCondition.cs | 3 + .../AbilityConditionAffliction.cs | 29 + .../AbilityConditionLocation.cs | 41 + .../Talents/Abilities/AbilityInterfaces.cs | 5 + .../Talents/Abilities/AbilityObjects.cs | 34 +- ...racterAbilityApplyStatusEffectsToAllies.cs | 9 +- .../Abilities/CharacterAbilityGiveMoney.cs | 5 +- .../CharacterAbilityGivePermanentStat.cs | 6 +- .../CharacterAbilityGiveResistance.cs | 2 +- .../CharacterAbilityModifyStatToLevel.cs | 35 + .../CharacterAbilitySpawnItemsToContainer.cs | 9 +- .../Abilities/CharacterAbilityUnlockTree.cs | 30 + ...ine.cs => CharacterAbilityAtmosMachine.cs} | 4 +- .../CharacterAbilityStonewall.cs | 42 - .../CharacterAbilityGroupEffect.cs | 1 + .../CharacterAbilityGroupInterval.cs | 4 + .../Characters/Talents/TalentTree.cs | 2 +- .../BarotraumaShared/SharedSource/Enums.cs | 9 +- .../SharedSource/Events/EventManager.cs | 1 + .../Events/Missions/MineralMission.cs | 73 +- .../Events/Missions/PirateMission.cs | 6 +- .../Extensions/ColorExtensions.cs | 6 +- .../GameSession/GameModes/CampaignMode.cs | 11 +- .../SharedSource/GameSession/GameSession.cs | 5 +- .../SharedSource/GameSettings.cs | 236 +----- .../Items/Components/GeneticMaterial.cs | 34 +- .../Items/Components/Holdable/Holdable.cs | 5 + .../Items/Components/Holdable/MeleeWeapon.cs | 33 +- .../Items/Components/Holdable/RangedWeapon.cs | 3 +- .../Items/Components/Holdable/RepairTool.cs | 2 +- .../Components/Machines/Deconstructor.cs | 31 +- .../Items/Components/Machines/Engine.cs | 4 +- .../Items/Components/Machines/Fabricator.cs | 37 +- .../Items/Components/Machines/Pump.cs | 4 +- .../Items/Components/Machines/Steering.cs | 2 +- .../Items/Components/Projectile.cs | 7 +- .../Items/Components/Repairable.cs | 11 +- .../Components/Signal/RegExFindComponent.cs | 1 + .../SharedSource/Items/Components/Turret.cs | 34 +- .../SharedSource/Items/Components/Wearable.cs | 26 +- .../SharedSource/Items/ItemPrefab.cs | 2 + .../SharedSource/Items/RelatedItem.cs | 3 +- .../SharedSource/Map/Explosion.cs | 2 +- .../SharedSource/Map/Levels/Level.cs | 8 + .../SharedSource/Map/Map/Location.cs | 28 +- .../SharedSource/Map/Map/Map.cs | 11 +- .../SharedSource/Map/PriceInfo.cs | 14 +- .../Primitives/Message/IReadMessage.cs | 2 + .../Primitives/Message/IWriteMessage.cs | 2 + .../Networking/Primitives/Message/Message.cs | 79 ++ .../Serialization/SerializableProperty.cs | 2 +- .../SharedSource/Sprite/Sprite.cs | 2 +- .../SharedSource/TextManager.cs | 20 +- .../SharedSource/Utils/SafeIO.cs | 23 +- Barotrauma/BarotraumaShared/TintTest.png | Bin 0 -> 91346 bytes Barotrauma/BarotraumaShared/changelog.txt | 35 + .../Graphics/SpriteBatch.cs | 191 +++-- .../Graphics/SpriteBatchItem.cs | 3 +- .../Graphics/SpriteBatcher.cs | 32 +- 124 files changed, 3571 insertions(+), 1848 deletions(-) create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Items/Components/Quality.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs create mode 100644 Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs rename Barotrauma/BarotraumaClient/ClientSource/Screens/{CampaignSetupUI.cs => CampaignSetupUI/SinglePlayerCampaignSetupUI.cs} (51%) create mode 100644 Barotrauma/BarotraumaClient/Content/Effects/thresholdtint.xnb create mode 100644 Barotrauma/BarotraumaClient/Content/Effects/thresholdtint_opengl.xnb create mode 100644 Barotrauma/BarotraumaClient/Shaders/thresholdtint.fx create mode 100644 Barotrauma/BarotraumaClient/Shaders/thresholdtint_opengl.fx create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAffliction.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionLocation.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToLevel.cs create mode 100644 Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityUnlockTree.cs rename Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/{CharacterAbilityEnigmaMachine.cs => CharacterAbilityAtmosMachine.cs} (86%) delete mode 100644 Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityStonewall.cs rename Barotrauma/{BarotraumaClient/ClientSource => BarotraumaShared/SharedSource}/Extensions/ColorExtensions.cs (69%) create mode 100644 Barotrauma/BarotraumaShared/TintTest.png diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index 893fb4761..2ab8174b8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -23,12 +23,30 @@ namespace Barotrauma private Sprite disguisedJobIcon; private Color disguisedJobColor; + private Sprite tintMask; + private float tintHighlightThreshold; + private float tintHighlightMultiplier; + public static void Init() { infoAreaPortraitBG = GUI.Style.GetComponentStyle("InfoAreaPortraitBG")?.GetDefaultSprite(); new Sprite("Content/UI/InventoryUIAtlas.png", new Rectangle(833, 298, 142, 98), null, 0); } + partial void LoadHeadSpriteProjectSpecific(XElement limbElement) + { + XElement maskElement = limbElement.Element("tintmask"); + if (maskElement != null) + { + string tintMaskPath = maskElement.GetAttributeString("texture", ""); + if (!string.IsNullOrWhiteSpace(tintMaskPath)) + { + tintMask = new Sprite(maskElement, file: Limb.GetSpritePath(tintMaskPath, this)); + tintHighlightThreshold = maskElement.GetAttributeFloat("highlightthreshold", 0.6f); + tintHighlightMultiplier = maskElement.GetAttributeFloat("highlightmultiplier", 0.8f); + } + } + } public GUIComponent CreateInfoFrame(GUIFrame frame, bool returnParent, Sprite permissionIcon = null) { @@ -458,6 +476,7 @@ namespace Barotrauma } else { + //TODO: disguise skin and hair colors sheetIndex = disguisedSheetIndex; portraitToDraw = disguisedPortrait; attachmentsToDraw = disguisedAttachmentSprites; @@ -465,22 +484,74 @@ namespace Barotrauma if (portraitToDraw != null) { + var currEffect = spriteBatch.GetCurrentEffect(); // Scale down the head sprite 10% float scale = targetWidth * 0.9f / Portrait.size.X; if (sheetIndex.HasValue) { + SetHeadEffect(spriteBatch); portraitToDraw.SourceRect = new Rectangle(CalculateOffset(portraitToDraw, sheetIndex.Value.ToPoint()), portraitToDraw.SourceRect.Size); } - portraitToDraw.Draw(spriteBatch, screenPos + offset, Color.White, 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) { - DrawAttachmentSprite(spriteBatch, attachment, portraitToDraw, sheetIndex, screenPos + offset, scale, depthStep, flip ? SpriteEffects.FlipHorizontally : SpriteEffects.None); + SetAttachmentEffect(spriteBatch, attachment); + DrawAttachmentSprite(spriteBatch, attachment, portraitToDraw, sheetIndex, screenPos + offset, scale, depthStep, GetAttachmentColor(attachment), flip ? SpriteEffects.FlipHorizontally : SpriteEffects.None); depthStep += depthStep; } } + spriteBatch.SwapEffect(currEffect); + } + } + + //TODO: I hate this so much :( + private SpriteBatch.EffectWithParams headEffectParameters; + private Dictionary attachmentEffectParameters + = new Dictionary(); + + private void SetHeadEffect(SpriteBatch spriteBatch) + { + headEffectParameters.Effect ??= GameMain.GameScreen.ThresholdTintEffect; + headEffectParameters.Params ??= new Dictionary(); + headEffectParameters.Params["xBaseTexture"] = headSprite.Texture; + headEffectParameters.Params["xTintMaskTexture"] = tintMask?.Texture ?? GUI.WhiteTexture; + headEffectParameters.Params["xCutoffTexture"] = GUI.WhiteTexture; + headEffectParameters.Params["baseToCutoffSizeRatio"] = 1.0f; + headEffectParameters.Params["highlightThreshold"] = tintHighlightThreshold; + headEffectParameters.Params["highlightMultiplier"] = tintHighlightMultiplier; + spriteBatch.SwapEffect(headEffectParameters); + } + + private void SetAttachmentEffect(SpriteBatch spriteBatch, WearableSprite attachment) + { + if (!attachmentEffectParameters.ContainsKey(attachment.Type)) + { + attachmentEffectParameters.Add(attachment.Type, new SpriteBatch.EffectWithParams(GameMain.GameScreen.ThresholdTintEffect, new Dictionary())); + } + var parameters = attachmentEffectParameters[attachment.Type].Params; + parameters["xBaseTexture"] = attachment.Sprite.Texture; + parameters["xTintMaskTexture"] = GUI.WhiteTexture; + parameters["xCutoffTexture"] = GUI.WhiteTexture; + parameters["baseToCutoffSizeRatio"] = 1.0f; + parameters["highlightThreshold"] = tintHighlightThreshold; + parameters["highlightMultiplier"] = tintHighlightMultiplier; + spriteBatch.SwapEffect(attachmentEffectParameters[attachment.Type]); + } + + private Color GetAttachmentColor(WearableSprite attachment) + { + switch (attachment.Type) + { + case WearableType.Hair: + return HairColor; + case WearableType.Beard: + case WearableType.Moustache: + return FacialHairColor; + default: + return Color.White; } } @@ -489,34 +560,28 @@ namespace Barotrauma var headSprite = HeadSprite; if (headSprite != null) { + var currEffect = spriteBatch.GetCurrentEffect(); float scale = Math.Min(targetAreaSize.X / headSprite.size.X, targetAreaSize.Y / headSprite.size.Y); if (Head.SheetIndex.HasValue) { headSprite.SourceRect = new Rectangle(CalculateOffset(headSprite, Head.SheetIndex.Value.ToPoint()), headSprite.SourceRect.Size); } - headSprite.Draw(spriteBatch, screenPos, scale: scale); + SetHeadEffect(spriteBatch); + headSprite.Draw(spriteBatch, screenPos, scale: scale, color: SkinColor); if (AttachmentSprites != null) { float depthStep = 0.000001f; foreach (var attachment in AttachmentSprites) { - DrawAttachmentSprite(spriteBatch, attachment, headSprite, Head.SheetIndex, screenPos, scale, depthStep); + SetAttachmentEffect(spriteBatch, attachment); + DrawAttachmentSprite(spriteBatch, attachment, headSprite, Head.SheetIndex, screenPos, scale, depthStep, GetAttachmentColor(attachment)); depthStep += depthStep; } } + spriteBatch.SwapEffect(currEffect); } } - public void DrawJobIcon(SpriteBatch spriteBatch, Vector2 pos, float scale = 1.0f, bool evaluateDisguise = false) - { - if (evaluateDisguise && IsDisguised) return; - var icon = !IsDisguisedAsAnother || !evaluateDisguise ? Job?.Prefab?.Icon : disguisedJobIcon; - if (icon == null) { return; } - Color iconColor = !IsDisguisedAsAnother || !evaluateDisguise ? Job.Prefab.UIColor : disguisedJobColor; - - icon.Draw(spriteBatch, pos, iconColor, scale: scale); - } - public void DrawJobIcon(SpriteBatch spriteBatch, Rectangle area, bool evaluateDisguise = false) { if (evaluateDisguise && IsDisguised) return; @@ -527,7 +592,7 @@ namespace Barotrauma icon.Draw(spriteBatch, area.Center.ToVector2(), iconColor, scale: Math.Min(area.Width / (float)icon.SourceRect.Width, area.Height / (float)icon.SourceRect.Height)); } - private void DrawAttachmentSprite(SpriteBatch spriteBatch, WearableSprite attachment, Sprite head, Vector2? sheetIndex, Vector2 drawPos, float scale, float depthStep, SpriteEffects spriteEffects = SpriteEffects.None) + private void DrawAttachmentSprite(SpriteBatch spriteBatch, WearableSprite attachment, Sprite head, Vector2? sheetIndex, Vector2 drawPos, float scale, float depthStep, Color? color = null, SpriteEffects spriteEffects = SpriteEffects.None) { if (attachment.InheritSourceRect) { @@ -544,7 +609,7 @@ namespace Barotrauma attachment.Sprite.SourceRect = head.SourceRect; } } - Vector2 origin = attachment.Sprite.Origin; + Vector2 origin; if (attachment.InheritOrigin) { origin = head.Origin; @@ -559,7 +624,7 @@ namespace Barotrauma { depth = head.Depth - depthStep; } - attachment.Sprite.Draw(spriteBatch, drawPos, Color.White, origin, rotate: 0, scale: scale, depth: depth, spriteEffect: spriteEffects); + attachment.Sprite.Draw(spriteBatch, drawPos, color ?? Color.White, origin, rotate: 0, scale: scale, depth: depth, spriteEffect: spriteEffects); } public static CharacterInfo ClientRead(string speciesName, IReadMessage inc) @@ -574,6 +639,9 @@ namespace Barotrauma int beardIndex = inc.ReadByte(); int moustacheIndex = inc.ReadByte(); int faceAttachmentIndex = inc.ReadByte(); + Color skinColor = inc.ReadColorR8G8B8(); + Color hairColor = inc.ReadColorR8G8B8(); + Color facialHairColor = inc.ReadColorR8G8B8(); string ragdollFile = inc.ReadString(); string jobIdentifier = inc.ReadString(); @@ -599,6 +667,9 @@ namespace Barotrauma ID = infoID, }; ch.RecreateHead(headSpriteID,(Race)race, (Gender)gender, hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex); + ch.SkinColor = skinColor; + ch.HairColor = hairColor; + ch.FacialHairColor = facialHairColor; if (ch.Job != null) { foreach (KeyValuePair skill in skillLevels) @@ -627,5 +698,384 @@ namespace Barotrauma ch.AdditionalTalentPoints = inc.ReadUInt16(); return ch; } + + public void CreateIcon(RectTransform rectT) + { + LoadHeadAttachments(); + new GUICustomComponent(rectT, + onDraw: (sb, component) => DrawIcon(sb, component.Rect.Center.ToVector2(), targetAreaSize: component.Rect.Size.ToVector2())); + } + + public class AppearanceCustomizationMenu : IDisposable + { + public readonly CharacterInfo CharacterInfo; + public GUIListBox HeadSelectionList = null; + public bool HasIcon = true; + + public GUIScrollBar.OnMovedHandler OnSliderMoved = null; + public GUIScrollBar.OnMovedHandler OnSliderReleased = null; + public Action OnHeadSwitch = null; + + private readonly GUIComponent parentComponent; + private readonly List characterSprites = new List(); + + public AppearanceCustomizationMenu(CharacterInfo info, GUIComponent parent, bool hasIcon = true) + { + CharacterInfo = info; + parentComponent = parent; + HasIcon = hasIcon; + + RecreateFrameContents(); + } + + public void RecreateFrameContents() + { + var info = CharacterInfo; + + HeadSelectionList = null; + parentComponent.ClearChildren(); + 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; + + info.LoadHeadAttachments(); + if (HasIcon) + { + info.CreateIcon( + new RectTransform(new Vector2(0.25f, 1.0f), parentComponent.RectTransform, Anchor.CenterRight) + { RelativeOffset = new Vector2(-0.01f, 0.0f) }); + } + + RectTransform createItemRectTransform(string labelTag, float width = 0.6f) + { + var layoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.2f), content.RectTransform)); + + var label = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), layoutGroup.RectTransform), + TextManager.Get(labelTag), font: GUI.SubHeadingFont); + + var bottomItem = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.5f), layoutGroup.RectTransform), + style: null); + + return new RectTransform(new Vector2(width, 1.0f), bottomItem.RectTransform, Anchor.Center); + } + + RectTransform genderItemRT = createItemRectTransform("Gender", 1.0f); + + GUILayoutGroup genderContainer = + new GUILayoutGroup(genderItemRT, isHorizontal: true) + { + Stretch = true, + RelativeSpacing = 0.05f + }; + + void createGenderButton(Gender gender) + { + new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), genderContainer.RectTransform), + TextManager.Get(gender.ToString()), style: "ListBoxElement") + { + UserData = gender, + OnClicked = OpenHeadSelection, + Selected = info.Gender == gender + }; + } + + createGenderButton(Gender.Male); + createGenderButton(Gender.Female); + + int countAttachmentsOfType(WearableType wearableType) + => info.FilterByTypeAndHeadID( + info.FilterElementsByGenderAndRace(info.Wearables, info.Head.gender, info.Head.race), + wearableType, info.HeadSpriteId).Count(); + + void createAttachmentSlider(int initialValue, WearableType wearableType) + { + int attachmentCount = countAttachmentsOfType(wearableType); + if (attachmentCount > 0) + { + var labelTag = wearableType == WearableType.FaceAttachment + ? "FaceAttachment.Accessories" + : $"FaceAttachment.{wearableType}"; + var sliderItemRT = createItemRectTransform(labelTag); + var slider = + new GUIScrollBar(sliderItemRT, style: "GUISlider") + { + Range = new Vector2(0, attachmentCount), + StepValue = 1, + OnMoved = (bar, scroll) => SwitchAttachment(bar, wearableType), + OnReleased = OnSliderReleased, + BarSize = 1.0f / (float)(attachmentCount + 1) + }; + slider.BarScrollValue = initialValue; + } + } + + createAttachmentSlider(info.HairIndex, WearableType.Hair); + createAttachmentSlider(info.BeardIndex, WearableType.Beard); + createAttachmentSlider(info.MoustacheIndex, WearableType.Moustache); + createAttachmentSlider(info.FaceAttachmentIndex, WearableType.FaceAttachment); + + void createColorSelector(string labelTag, IEnumerable options, Func getter, + Action setter) + { + var selectorItemRT = createItemRectTransform(labelTag, 0.4f); + var dropdown = + new GUIDropDown(selectorItemRT) + { AllowNonText = true }; + + var listBoxSize = dropdown.ListBox.RectTransform.RelativeSize; + dropdown.ListBox.RectTransform.RelativeSize = new Vector2(listBoxSize.X * 1.75f, listBoxSize.Y); + var dropdownButton = dropdown.GetChild(); + var buttonFrame = + new GUIFrame( + new RectTransform(Vector2.One * 0.7f, dropdownButton.RectTransform, Anchor.CenterLeft) + { RelativeOffset = new Vector2(0.05f, 0.0f) }, style: null); + dropdown.OnSelected = (component, color) => + { + setter((Color)color); + buttonFrame.Color = getter(); + buttonFrame.HoverColor = getter(); + return true; + }; + buttonFrame.Color = getter(); + buttonFrame.HoverColor = getter(); + + dropdown.ListBox.UseGridLayout = true; + foreach (var option in options) + { + var optionElement = + new GUIFrame( + new RectTransform(new Vector2(0.25f, 1.0f / 3.0f), + dropdown.ListBox.Content.RectTransform), + style: "ListBoxElement") + { + UserData = option, + CanBeFocused = true + }; + var colorElement = + new GUIFrame( + new RectTransform(Vector2.One * 0.75f, optionElement.RectTransform, Anchor.Center, + scaleBasis: ScaleBasis.Smallest), + style: null) + { + Color = option, + HoverColor = option, + OutlineColor = Color.Lerp(Color.Black, option, 0.5f), + CanBeFocused = false + }; + } + } + + if (countAttachmentsOfType(WearableType.Hair) > 0) + { + createColorSelector($"Customization.{nameof(info.HairColor)}", info.HairColors, + () => info.HairColor, (color) => info.HairColor = color); + } + + if (countAttachmentsOfType(WearableType.Moustache) > 0 || + countAttachmentsOfType(WearableType.Beard) > 0) + { + createColorSelector($"Customization.{nameof(info.FacialHairColor)}", info.FacialHairColors, + () => info.FacialHairColor, (color) => info.FacialHairColor = color); + } + + createColorSelector($"Customization.{nameof(info.SkinColor)}", info.SkinColors, () => info.SkinColor, + (color) => info.SkinColor = color); + } + + 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( + new RectTransform( + new Point(parentComponent.Rect.Width, + (int)(parentComponent.Rect.Width * characterHeightWidthRatio * 0.6f)), GUI.Canvas) + { + AbsoluteOffset = new Point(parentComponent.Rect.Right - parentComponent.Rect.Width, + button.Rect.Bottom) + }); + + parentComponent.RectTransform.SizeChanged += () => + { + if (parentComponent == null || HeadSelectionList?.RectTransform == null || button == null) + { + return; + } + + HeadSelectionList.RectTransform.Resize(new Point(parentComponent.Rect.Width, + (int)(parentComponent.Rect.Width * characterHeightWidthRatio * 0.6f))); + HeadSelectionList.RectTransform.AbsoluteOffset = + new Point(parentComponent.Rect.Right - parentComponent.Rect.Width, button.Rect.Bottom); + }; + + new GUIFrame( + new RectTransform(new Vector2(1.25f, 1.25f), HeadSelectionList.RectTransform, Anchor.Center), + style: "OuterGlow", color: Color.Black) + { + UserData = "outerglow", + CanBeFocused = false + }; + + GUILayoutGroup row = null; + int itemsInRow = 0; + + XElement headElement = info.Ragdoll.MainElement.Elements().FirstOrDefault(e => + e.GetAttributeString("type", "").Equals("head", StringComparison.OrdinalIgnoreCase)); + XElement headSpriteElement = headElement.Element("sprite"); + string spritePathWithTags = headSpriteElement.Attribute("texture").Value; + + var characterConfigElement = info.CharacterConfigElement; + + var heads = info.Heads; + if (heads != null) + { + row = null; + itemsInRow = 0; + foreach (var head in heads) + { + var headPreset = head.Key; + Gender gender = headPreset.Gender; + Race race = headPreset.Race; + int headIndex = headPreset.ID; + + string spritePath = spritePathWithTags + .Replace("[GENDER]", gender.ToString().ToLowerInvariant()) + .Replace("[RACE]", race.ToString().ToLowerInvariant()); + + if (!File.Exists(spritePath)) + { + continue; + } + + Sprite headSprite = new Sprite(headSpriteElement, "", spritePath); + headSprite.SourceRect = + new Rectangle(CharacterInfo.CalculateOffset(headSprite, head.Value.ToPoint()), + headSprite.SourceRect.Size); + characterSprites.Add(headSprite); + + if (itemsInRow >= 4 || row == null || gender != (Gender)row.UserData) + { + row = new GUILayoutGroup( + new RectTransform(new Vector2(1.0f, 0.333f), HeadSelectionList.Content.RectTransform), + true) + { + UserData = gender, + Visible = gender == selectedGender + }; + itemsInRow = 0; + } + + var btn = new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), row.RectTransform), + style: "ListBoxElementSquare") + { + OutlineColor = Color.White * 0.5f, + PressedColor = Color.White * 0.5f, + UserData = new Tuple(gender, race, headIndex), + OnClicked = SwitchHead, + Selected = gender == info.Gender && race == info.Race && headIndex == info.HeadSpriteId, + Visible = gender == selectedGender + }; + + new GUIImage(new RectTransform(Vector2.One, btn.RectTransform), headSprite, scaleToFit: true); + itemsInRow++; + } + } + + return false; + } + + private bool SwitchHead(GUIButton button, object obj) + { + var info = CharacterInfo; + Gender gender = ((Tuple)obj).Item1; + Race race = ((Tuple)obj).Item2; + int id = ((Tuple)obj).Item3; + info.Gender = gender; + info.Race = race; + info.HeadSpriteId = id; + RecreateFrameContents(); + OnHeadSwitch?.Invoke(this); + return true; + } + + private bool SwitchAttachment(GUIScrollBar scrollBar, WearableType type) + { + var info = CharacterInfo; + int index = (int)scrollBar.BarScrollValue; + switch (type) + { + case WearableType.Beard: + info.BeardIndex = index; + break; + case WearableType.FaceAttachment: + info.FaceAttachmentIndex = index; + break; + case WearableType.Hair: + info.HairIndex = index; + break; + case WearableType.Moustache: + info.MoustacheIndex = index; + break; + default: + DebugConsole.ThrowError($"Wearable type not implemented: {type}"); + return false; + } + + info.RefreshHead(); + OnSliderMoved?.Invoke(scrollBar, scrollBar.BarScroll); + return true; + } + + public void Update() + { + if (HeadSelectionList != null && PlayerInput.PrimaryMouseButtonDown() && + !GUI.IsMouseOn(HeadSelectionList)) + { + HeadSelectionList.Visible = false; + } + } + + public void AddToGUIUpdateList() + { + HeadSelectionList?.AddToGUIUpdateList(); + } + + private void ClearSprites() + { + foreach (Sprite sprite in characterSprites) { sprite.Remove(); } + characterSprites.Clear(); + } + + public void Dispose() + { + ClearSprites(); + } + + ~AppearanceCustomizationMenu() + { + Dispose(); + } + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index 2e648839a..78fa27ccf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -601,8 +601,13 @@ namespace Barotrauma .FindAll(a => a.ShouldShowIcon(Character) && a.Prefab.Icon != null); currentDisplayedAfflictions.Sort((a1, a2) => { - int dmgPerSecond = Math.Sign(a2.DamagePerSecond - a1.DamagePerSecond); - return dmgPerSecond != 0 ? dmgPerSecond : Math.Sign(a1.Strength - a1.Strength); + int dmgPerSecond = Math.Sign(a1.DamagePerSecond - a2.DamagePerSecond); + if (dmgPerSecond != 0) { return dmgPerSecond; } + return Math.Sign(GetStr(a1) - GetStr(a2)); + static float GetStr(Affliction affliction) + { + return affliction.Strength / affliction.Prefab.MaxStrength * (affliction.Prefab.IsBuff ? 1.0f : 10.0f); + } }); HintManager.OnAfflictionDisplayed(Character, currentDisplayedAfflictions); updateDisplayedAfflictionsTimer = UpdateDisplayedAfflictionsInterval; @@ -1131,6 +1136,8 @@ namespace Barotrauma public static Color GetAfflictionIconColor(Affliction affliction) => GetAfflictionIconColor(affliction.Prefab, affliction); + private readonly List<(Affliction affliction, float strength)> displayedAfflictions = new List<(Affliction affliction, float strength)>(); + private void UpdateAfflictionContainer(LimbHealth selectedLimb) { if (selectedLimb == null) @@ -1139,45 +1146,33 @@ namespace Barotrauma return; } var currentAfflictions = GetMatchingAfflictions(selectedLimb, a => a.ShouldShowIcon(Character)); - var displayedAfflictions = afflictionIconContainer.Content.Children.Select(c => c.UserData as Affliction); - if (currentAfflictions.Any(a => !displayedAfflictions.Contains(a)) || - displayedAfflictions.Any(a => !currentAfflictions.Contains(a))) + if (currentAfflictions.Any(a => !displayedAfflictions.Any(d => d.affliction == a)) || + displayedAfflictions.Any(a => !currentAfflictions.Contains(a.affliction))) { CreateAfflictionInfos(currentAfflictions); + CreateRecommendedTreatments(); + } + //update recommended treatments if the strength of some displayed affliction has changed by > 1 + else if (displayedAfflictions.Any(d => Math.Abs(d.strength - currentAfflictions.First(a => a == d.affliction).Strength) > 1.0f)) + { + CreateRecommendedTreatments(); } - UpdateAfflictionInfos(displayedAfflictions); + UpdateAfflictionInfos(displayedAfflictions.Select(d => d.affliction)); } private void CreateAfflictionInfos(IEnumerable afflictions) { afflictionIconContainer.ClearChildren(); - recommendedTreatmentContainer.Content.ClearChildren(); - - float characterSkillLevel = Character.Controlled == null ? 0.0f : Character.Controlled.GetSkillLevel("medical"); - - //key = item identifier - //float = suitability - Dictionary treatmentSuitability = new Dictionary(); - GetSuitableTreatments(treatmentSuitability, - normalize: true, - ignoreHiddenAfflictions: true, - limb: selectedLimbIndex == -1 ? null : Character.AnimController.Limbs.Find(l => l.HealthIndex == selectedLimbIndex)); - - foreach (string treatment in treatmentSuitability.Keys.ToList()) - { - //prefer suggestions for items the player has - if (Character.Controlled.Inventory.FindItemByIdentifier(treatment) != null) - { - treatmentSuitability[treatment] *= 10.0f; - } - } + displayedAfflictions.Clear(); Affliction mostSevereAffliction = SortAfflictionsBySeverity(afflictions).FirstOrDefault(); GUIButton buttonToSelect = null; foreach (Affliction affliction in afflictions) { + displayedAfflictions.Add((affliction, affliction.Strength)); + var child = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.3f), afflictionIconContainer.Content.RectTransform, Anchor.TopCenter)) { Stretch = true, @@ -1233,6 +1228,39 @@ namespace Barotrauma child.Recalculate(); } + buttonToSelect?.OnClicked(buttonToSelect, "selectaffliction"); + afflictionIconContainer.RecalculateChildren(); + } + + private void CreateRecommendedTreatments() + { + ItemPrefab prevHighlightedItem = null; + if (GUI.MouseOn?.UserData is ItemPrefab && recommendedTreatmentContainer.Content.IsParentOf(GUI.MouseOn)) + { + prevHighlightedItem = (ItemPrefab)GUI.MouseOn.UserData; + } + + recommendedTreatmentContainer.Content.ClearChildren(); + + float characterSkillLevel = Character.Controlled == null ? 0.0f : Character.Controlled.GetSkillLevel("medical"); + + //key = item identifier + //float = suitability + Dictionary treatmentSuitability = new Dictionary(); + GetSuitableTreatments(treatmentSuitability, + normalize: true, + ignoreHiddenAfflictions: true, + limb: selectedLimbIndex == -1 ? null : Character.AnimController.Limbs.Find(l => l.HealthIndex == selectedLimbIndex)); + + foreach (string treatment in treatmentSuitability.Keys.ToList()) + { + //prefer suggestions for items the player has + if (Character.Controlled.Inventory.FindItemByIdentifier(treatment) != null) + { + treatmentSuitability[treatment] *= 10.0f; + } + } + if (!treatmentSuitability.Any()) { new GUITextBlock(new RectTransform(Vector2.One, recommendedTreatmentContainer.Content.RectTransform), TextManager.Get("none"), textAlignment: Alignment.Center) @@ -1248,10 +1276,6 @@ namespace Barotrauma recommendedTreatmentContainer.AutoHideScrollBar = true; } - buttonToSelect?.OnClicked(buttonToSelect, "selectaffliction"); - - afflictionIconContainer.RecalculateChildren(); - List> treatmentSuitabilities = treatmentSuitability.OrderByDescending(t => t.Value).ToList(); int count = 0; @@ -1286,7 +1310,7 @@ namespace Barotrauma new GUIImage(new RectTransform(Vector2.One, innerFrame.RectTransform, Anchor.Center), style: "TalentBackgroundGlow") { CanBeFocused = false, - Color = Color.White * 0.7f, + Color = GUI.Style.Green, HoverColor = Color.White, PressedColor = Color.DarkGray, SelectedColor = Color.Transparent, @@ -1304,6 +1328,12 @@ namespace Barotrauma SelectedColor = itemColor, DisabledColor = itemColor * 0.8f }; + + if (item == prevHighlightedItem) + { + innerFrame.State = GUIComponent.ComponentState.Hover; + innerFrame.Children.ForEach(c => c.State = GUIComponent.ComponentState.Hover); + } } recommendedTreatmentContainer.RecalculateChildren(); @@ -1315,6 +1345,19 @@ namespace Barotrauma int dmgPerSecond = Math.Sign(second.DamagePerSecond - first.DamagePerSecond); return dmgPerSecond != 0 ? dmgPerSecond : Math.Sign(second.Strength - first.Strength); }); + + if (count > 0) + { + var treatmentIconSize = recommendedTreatmentContainer.Content.Children.Sum(c => c.Rect.Width + recommendedTreatmentContainer.Spacing); + if (treatmentIconSize < recommendedTreatmentContainer.Content.Rect.Width) + { + var spacing = new GUIFrame(new RectTransform(new Point((recommendedTreatmentContainer.Content.Rect.Width - treatmentIconSize) / 2, 0), recommendedTreatmentContainer.Content.RectTransform), style: null) + { + CanBeFocused = false + }; + spacing.RectTransform.SetAsFirstChild(); + } + } } private void CreateAfflictionInfoElements(GUIComponent parent, Affliction affliction) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs index c9c4d7300..d221c34b7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs @@ -120,6 +120,15 @@ namespace Barotrauma public List ActiveDeformations { get; set; } = new List(); public Sprite Sprite { get; protected set; } + public Sprite TintMask { get; protected set; } + + public Sprite HuskMask { get; protected set; } + public float TintHighlightThreshold { get; protected set; } + public float TintHighlightMultiplier { get; protected set; } + + private SpriteBatch.EffectWithParams tintEffectParams; + private SpriteBatch.EffectWithParams huskSpriteParams; + protected DeformableSprite _deformSprite; @@ -273,6 +282,7 @@ namespace Barotrauma DecorativeSpriteGroups[groupID].Add(decorativeSprite); spriteAnimState.Add(decorativeSprite, new SpriteState()); } + TintMask = null; foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -308,6 +318,22 @@ namespace Barotrauma InitialLightSourceColor = LightSource.Color; InitialLightSpriteAlpha = LightSource.OverrideLightSpriteAlpha; break; + case "tintmask": + string tintMaskPath = subElement.GetAttributeString("texture", ""); + if (!string.IsNullOrWhiteSpace(tintMaskPath)) + { + TintMask = new Sprite(subElement, file: GetSpritePath(tintMaskPath)); + TintHighlightThreshold = subElement.GetAttributeFloat("highlightthreshold", 0.6f); + TintHighlightMultiplier = subElement.GetAttributeFloat("highlightmultiplier", 0.8f); + } + break; + case "huskmask": + string huskMaskPath = subElement.GetAttributeString("texture", ""); + if (!string.IsNullOrWhiteSpace(huskMaskPath)) + { + HuskMask = new Sprite(subElement, file: GetSpritePath(huskMaskPath)); + } + break; } ISerializableEntity GetConditionalTarget() @@ -449,20 +475,20 @@ namespace Barotrauma /// /// Get the full path of a limb sprite, taking into account tags, gender and head id /// - private string GetSpritePath(string texturePath) + public static string GetSpritePath(string texturePath, CharacterInfo characterInfo) { string spritePath = texturePath; string spritePathWithTags = spritePath; - if (character.Info != null && character.IsHumanoid) + if (characterInfo != null) { - spritePath = spritePath.Replace("[GENDER]", (character.Info.Gender == Gender.Female) ? "female" : "male"); - spritePath = spritePath.Replace("[RACE]", character.Info.Race.ToString().ToLowerInvariant()); - spritePath = spritePath.Replace("[HEADID]", character.Info.HeadSpriteId.ToString()); + spritePath = spritePath.Replace("[GENDER]", (characterInfo.Gender == Gender.Female) ? "female" : "male"); + spritePath = spritePath.Replace("[RACE]", characterInfo.Race.ToString().ToLowerInvariant()); + spritePath = spritePath.Replace("[HEADID]", characterInfo.HeadSpriteId.ToString()); - if (character.Info.HeadSprite != null && character.Info.SpriteTags.Any()) + if (characterInfo.HeadSprite != null && characterInfo.SpriteTags.Any()) { string tags = ""; - character.Info.SpriteTags.ForEach(tag => tags += "[" + tag + "]"); + characterInfo.SpriteTags.ForEach(tag => tags += "[" + tag + "]"); spritePathWithTags = Path.Combine( Path.GetDirectoryName(spritePath), @@ -472,6 +498,13 @@ namespace Barotrauma return File.Exists(spritePathWithTags) ? spritePathWithTags : spritePath; } + + private string GetSpritePath(string texturePath) + { + if (!character.IsHumanoid) { return texturePath; } + return GetSpritePath(texturePath, character?.Info); + } + partial void LoadParamsProjSpecific() { bool isFlipped = dir == Direction.Left; @@ -638,13 +671,24 @@ namespace Barotrauma var spriteParams = Params.GetSprite(); if (spriteParams == null) { return; } - Color color = new Color(spriteParams.Color.R / 255f * brightness, spriteParams.Color.G / 255f * brightness, spriteParams.Color.B / 255f * brightness, spriteParams.Color.A / 255f); + Color clr = spriteParams.Color; + if (!spriteParams.IgnoreTint) + { + clr = clr.Multiply(ragdoll.RagdollParams.Color); + if (character.Info != null) + { + clr = clr.Multiply(character.Info.SkinColor); + } + } + 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); if (deadTimer > 0) { color = Color.Lerp(color, spriteParams.DeadColor, MathUtils.InverseLerp(0, spriteParams.DeadColorTime, deadTimer)); } color = overrideColor ?? color; + blankColor = overrideColor ?? blankColor; if (isSevered) { @@ -667,6 +711,8 @@ namespace Barotrauma OtherWearables.Any(w => w.HideLimb) || wearingItems.Any(w => w != null && w.HideLimb); + bool drawHuskSprite = HuskSprite != null && !wearableTypesToHide.Contains(WearableType.Husk); + var activeSprite = ActiveSprite; if (type == LimbType.Head) { @@ -698,7 +744,33 @@ namespace Barotrauma } else { + bool useTintMask = TintMask != null && spriteBatch.GetCurrentEffect() is null; + if (useTintMask) + { + tintEffectParams.Effect ??= GameMain.GameScreen.ThresholdTintEffect; + tintEffectParams.Params ??= new Dictionary(); + var parameters = tintEffectParams.Params; + parameters["xBaseTexture"] = Sprite.Texture; + parameters["xTintMaskTexture"] = TintMask.Texture; + if (drawHuskSprite && HuskMask != null) + { + parameters["xCutoffTexture"] = HuskMask.Texture; + parameters["baseToCutoffSizeRatio"] = (float)Sprite.Texture.Width / (float)HuskMask.Texture.Width; + } + else + { + parameters["xCutoffTexture"] = GUI.WhiteTexture; + parameters["baseToCutoffSizeRatio"] = 1.0f; + } + parameters["highlightThreshold"] = TintHighlightThreshold; + parameters["highlightMultiplier"] = TintHighlightMultiplier; + spriteBatch.SwapEffect(tintEffectParams); + } body.Draw(spriteBatch, activeSprite, color, null, Scale * TextureScale, Params.MirrorHorizontally, Params.MirrorVertically); + if (useTintMask) + { + spriteBatch.SwapEffect(null); + } } // Handle non-exlusive, i.e. additional conditional sprites foreach (var conditionalSprite in ConditionalSprites) @@ -770,15 +842,36 @@ namespace Barotrauma } if (onlyDrawable == null) { - if (HerpesSprite != null && !wearableTypesToHide.Contains(WearableType.Herpes)) + if (HerpesSprite != null && !wearableTypesToHide.Contains(WearableType.Herpes) && herpesStrength > 0) { - DrawWearable(HerpesSprite, depthStep, spriteBatch, color * Math.Min(herpesStrength / 10.0f, 1.0f), spriteEffect); + float alpha = Math.Min(herpesStrength * 2 / 100.0f, 1.0f); + DrawWearable(HerpesSprite, depthStep, spriteBatch, blankColor, alpha: alpha, spriteEffect); + depthStep += step; + } + if (drawHuskSprite) + { + bool useTintEffect = HuskMask != null && spriteBatch.GetCurrentEffect() is null; + if (useTintEffect) + { + huskSpriteParams.Effect ??= GameMain.GameScreen.ThresholdTintEffect; + huskSpriteParams.Params ??= new Dictionary(); + var parameters = huskSpriteParams.Params; + parameters["xCutoffTexture"] = GUI.WhiteTexture; + parameters["baseToCutoffSizeRatio"] = 1.0f; + spriteBatch.SwapEffect(huskSpriteParams); + } + DrawWearable(HuskSprite, depthStep, spriteBatch, color, alpha: color.A / 255f, spriteEffect); + if (useTintEffect) + { + spriteBatch.SwapEffect(null); + } depthStep += step; } foreach (WearableSprite wearable in OtherWearables) { + if (wearable.Type == WearableType.Husk) { continue; } if (wearableTypesToHide.Contains(wearable.Type)) { continue; } - DrawWearable(wearable, depthStep, spriteBatch, color, spriteEffect); + DrawWearable(wearable, depthStep, spriteBatch, blankColor, alpha: color.A / 255f, spriteEffect); //if there are multiple sprites on this limb, make the successive ones be drawn in front depthStep += step; } @@ -786,7 +879,7 @@ namespace Barotrauma foreach (WearableSprite wearable in WearingItems) { if (onlyDrawable != null && onlyDrawable != wearable) continue; - DrawWearable(wearable, depthStep, spriteBatch, color, spriteEffect); + DrawWearable(wearable, depthStep, spriteBatch, blankColor, alpha: color.A / 255f, spriteEffect); //if there are multiple sprites on this limb, make the successive ones be drawn in front depthStep += step; } @@ -936,7 +1029,7 @@ namespace Barotrauma } } - private void DrawWearable(WearableSprite wearable, float depthStep, SpriteBatch spriteBatch, Color color, SpriteEffects spriteEffect) + private void DrawWearable(WearableSprite wearable, float depthStep, SpriteBatch spriteBatch, Color color, float alpha, SpriteEffects spriteEffect) { var sprite = ActiveSprite; if (wearable.InheritSourceRect) @@ -955,7 +1048,7 @@ namespace Barotrauma } } - Vector2 origin = wearable.Sprite.Origin; + Vector2 origin; if (wearable.InheritOrigin) { origin = sprite.Origin; @@ -986,7 +1079,7 @@ namespace Barotrauma Color wearableColor = Color.White; if (wearableItemComponent != null) { - // Draw outer cloths on top of inner cloths. + // Draw outer clothes on top of inner clothes. if (wearableItemComponent.AllowedSlots.Contains(InvSlotType.OuterClothes)) { depth -= depthStep; @@ -997,15 +1090,38 @@ namespace Barotrauma } wearableColor = wearableItemComponent.Item.GetSpriteColor(); } - float textureScale = wearable.InheritTextureScale ? TextureScale : wearable.Scale; - + else if (character.Info != null) + { + if (wearable.Type == WearableType.Hair) + { + wearableColor = character.Info.HairColor; + } + else if (wearable.Type == WearableType.Beard || wearable.Type == WearableType.Moustache) + { + wearableColor = character.Info.FacialHairColor; + } + } + float scale = wearable.Scale; + if (wearable.InheritScale) + { + if (!wearable.IgnoreTextureScale) + { + scale *= TextureScale; + } + if (!wearable.IgnoreLimbScale) + { + scale *= Params.Scale; + } + if (!wearable.IgnoreRagdollScale) + { + scale *= ragdoll.RagdollParams.LimbScale; + } + } float rotation = -body.DrawRotation - wearable.Rotation * Dir; - - wearable.Sprite.Draw(spriteBatch, - new Vector2(body.DrawPosition.X, -body.DrawPosition.Y), - new Color((color.R * wearableColor.R) / (255.0f * 255.0f), (color.G * wearableColor.G) / (255.0f * 255.0f), (color.B * wearableColor.B) / (255.0f * 255.0f)) * ((color.A * wearableColor.A) / (255.0f * 255.0f)), - origin, rotation, - Scale * textureScale, spriteEffect, depth); + float finalAlpha = alpha * wearableColor.A; + Color finalColor = color.Multiply(wearableColor); + finalColor = new Color(finalColor.R, finalColor.G, finalColor.B, (byte)finalAlpha); + wearable.Sprite.Draw(spriteBatch, new Vector2(body.DrawPosition.X, -body.DrawPosition.Y), finalColor, origin, rotation, scale, spriteEffect, depth); } private WearableSprite GetWearableSprite(WearableType type, bool random = false) @@ -1056,6 +1172,9 @@ namespace Barotrauma HerpesSprite?.Sprite.Remove(); HerpesSprite = null; + + TintMask?.Remove(); + TintMask = null; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs index 61e23e9a5..66eb75c8b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MineralMission.cs @@ -29,7 +29,7 @@ namespace Barotrauma } } - for (int i = 0; i < ResourceClusters.Count; i++) + for (int i = 0; i < resourceClusters.Count; i++) { var amount = msg.ReadByte(); var rotation = msg.ReadSingle(); @@ -41,20 +41,20 @@ namespace Barotrauma h.AttachToWall(); item.Rotation = rotation; } - if (SpawnedResources.TryGetValue(item.Prefab.Identifier, out var resources)) + if (spawnedResources.TryGetValue(item.Prefab.Identifier, out var resources)) { resources.Add(item); } else { - SpawnedResources.Add(item.Prefab.Identifier, new List() { item }); + spawnedResources.Add(item.Prefab.Identifier, new List() { item }); } } } CalculateMissionClusterPositions(); - for(int i = 0; i < ResourceClusters.Count; i++) + for(int i = 0; i < resourceClusters.Count; i++) { var identifier = msg.ReadString(); var count = msg.ReadByte(); @@ -66,7 +66,7 @@ namespace Barotrauma if (!(entity is Item item)) { continue; } resources[j] = item; } - RelevantLevelResources.Add(identifier, resources); + relevantLevelResources.Add(identifier, resources); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index e3141a7b8..a8f8dd950 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -130,6 +130,7 @@ namespace Barotrauma public static GUIStyle Style; private static Texture2D t; + public static Texture2D WhiteTexture => t; private static Sprite[] MouseCursorSprites => Style.CursorSprite; private static bool debugDrawSounds, debugDrawEvents, debugDrawMetadata; @@ -862,11 +863,10 @@ namespace Barotrauma int index = 0; if (updateList.Count > 0) { - index = updateList.Count - 1; - while (updateList[index].UpdateOrder > item.UpdateOrder) + index = updateList.Count; + while (index > 0 && updateList[index-1].UpdateOrder > item.UpdateOrder) { index--; - if (index == 0) { break; } } } if (!updateListSet.Contains(item)) @@ -1057,12 +1057,15 @@ namespace Barotrauma if (listBox.DraggedElement != null) { return CursorState.Dragging; } if (listBox.CanDragElements) { return CursorState.Move; } - var hoverParent = c; - while (true) + if (listBox.HoverCursor != CursorState.Default) { - if (hoverParent == parent || hoverParent == null) { break; } - if (hoverParent.State == GUIComponent.ComponentState.Hover) { return CursorState.Hand; } - hoverParent = hoverParent.Parent; + var hoverParent = c; + while (true) + { + if (hoverParent == parent || hoverParent == null) { break; } + if (hoverParent.State == GUIComponent.ComponentState.Hover) { return CursorState.Hand; } + hoverParent = hoverParent.Parent; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs index d1560e7ed..6c2780430 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs @@ -23,6 +23,8 @@ namespace Barotrauma private bool selectMultiple; public bool Dropped { get; set; } + + public bool AllowNonText { get; set; } public object SelectedItemData { @@ -318,9 +320,9 @@ namespace Barotrauma if (textBlock == null) { textBlock = component.GetChild(); - if (textBlock == null) return false; + if (textBlock is null && !AllowNonText) { return false; } } - button.Text = textBlock.Text; + button.Text = textBlock?.Text ?? ""; } Dropped = false; // TODO: OnSelected can be called multiple times and when it shouldn't be called -> turn into an event so that nobody else can call it. diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs index 3a6537aa0..f1fd031ff 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs @@ -11,7 +11,7 @@ namespace Barotrauma { private Dictionary componentStyles; - private XElement configElement; + private readonly XElement configElement; private GraphicsDevice graphicsDevice; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index 803f2537c..d62df1090 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -19,7 +19,7 @@ namespace Barotrauma private static UISprite spectateIcon, disconnectedIcon; private static Sprite ownerIcon, moderatorIcon; - public enum InfoFrameTab { Crew, Mission, Reputation, MyCharacter, Traitor, Submarine, Talents }; + public enum InfoFrameTab { Crew, Mission, Reputation, Traitor, Submarine, Talents }; public static InfoFrameTab selectedTab; private GUIFrame infoFrame, contentFrame; @@ -34,6 +34,8 @@ namespace Barotrauma private List teamIDs; private const string inLobbyString = "\u2022 \u2022 \u2022"; + private GUIFrame pendingChangesFrame = null; + public static Color OwnCharacterBGColor = Color.Gold * 0.7f; private class LinkedGUI @@ -134,6 +136,13 @@ namespace Barotrauma public void Update() { + GameSession.UpdateTalentNotificationIndicator(talentPointNotification); + if (Character.Controlled is { } controlled && talentResetButton != null && talentApplyButton != null) + { + int talentCount = selectedTalents.Count - controlled.Info.UnlockedTalents.Count; + talentResetButton.Enabled = talentApplyButton.Enabled = talentCount > 0; + } + if (selectedTab != InfoFrameTab.Crew) return; if (linkedGUIList == null) return; @@ -183,16 +192,8 @@ namespace Barotrauma new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, infoFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); //this used to be a switch expression but i changed it because it killed enc :( - Vector2 contentFrameSize; - switch (selectedTab) - { - case InfoFrameTab.MyCharacter: - contentFrameSize = new Vector2(0.45f, 0.5f); - break; - default: - contentFrameSize = new Vector2(0.45f, 0.667f); - break; - } + //now it's not even a switch statement anymore :( + Vector2 contentFrameSize = new Vector2(0.45f, 0.667f); contentFrame = new GUIFrame(new RectTransform(contentFrameSize, infoFrame.RectTransform, Anchor.TopCenter, Pivot.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.12f) }); var horizontalLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.958f, 0.943f), contentFrame.RectTransform, Anchor.TopCenter, Pivot.TopCenter) { AbsoluteOffset = new Point(0, GUI.IntScale(25f)) }, isHorizontal: true) @@ -243,6 +244,17 @@ namespace Barotrauma { TextGetter = () => TextManager.GetWithVariable("campaignmoney", "[money]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", campaignMode.Money)) }; + GUIFrame bottomDisclaimerFrame = new GUIFrame(new RectTransform(new Vector2(contentFrameSize.X, 0.1f), infoFrame.RectTransform) + { + AbsoluteOffset = new Point(contentFrame.Rect.X, contentFrame.Rect.Bottom + GUI.IntScale(8)) + }, style: null); + + pendingChangesFrame = new GUIFrame(new RectTransform(Vector2.One, bottomDisclaimerFrame.RectTransform, Anchor.Center), style: null); + + if (GameMain.NetLobbyScreen?.CampaignCharacterDiscarded ?? false) + { + NetLobbyScreen.CreateChangesPendingFrame(pendingChangesFrame); + } } else { @@ -255,20 +267,17 @@ namespace Barotrauma var submarineButton = createTabButton(InfoFrameTab.Submarine, "submarine"); - if (GameMain.NetworkMember != null) + var talentsButton = createTabButton(InfoFrameTab.Talents, "tabmenu.character"); + talentsButton.OnAddedToGUIUpdateList += (component) => { - var myCharacterButton = createTabButton(InfoFrameTab.MyCharacter, "tabmenu.character"); - } - - var talentsButton = createTabButton(InfoFrameTab.Talents, "tabmenu.talents"); - talentsButton.OnAddedToGUIUpdateList += (GUIComponent component) => - { - talentsButton.Enabled = Character.Controlled?.Info != null && (GameMain.GameSession?.Campaign != null || Screen.Selected == GameMain.TestScreen || GameMain.GameSession.GameMode is TestGameMode); + talentsButton.Enabled = Character.Controlled?.Info != null; if (!talentsButton.Enabled && selectedTab == InfoFrameTab.Talents) { SelectInfoFrameTab(null, InfoFrameTab.Crew); } }; + + talentPointNotification = GameSession.CreateTalentIconNotification(talentsButton); } private bool SelectInfoFrameTab(GUIButton button, object userData) @@ -300,10 +309,6 @@ namespace Barotrauma if (traitor == null || traitorMission == null) return false; CreateTraitorInfo(infoFrameHolder, traitorMission, traitor); break; - case InfoFrameTab.MyCharacter: - if (GameMain.NetworkMember == null) { return false; } - GameMain.NetLobbyScreen.CreatePlayerFrame(infoFrameHolder); - break; case InfoFrameTab.Submarine: CreateSubmarineInfo(infoFrameHolder, Submarine.MainSub); break; @@ -1188,6 +1193,11 @@ namespace Barotrauma private GUITextBlock talentPointText; private GUIListBox skillListBox; + private GUIButton talentApplyButton, + talentResetButton; + + private GUIImage talentPointNotification; + private readonly ImmutableDictionary talentStageStyles = new Dictionary { { TalentTree.TalentTreeStageState.Invalid, GUI.Style.GetComponentStyle("TalentTreeLocked") }, @@ -1219,9 +1229,22 @@ namespace Barotrauma int padding = GUI.IntScale(15); GUIFrame talentFrameContent = new GUIFrame(new RectTransform(new Point(talentFrameBackground.Rect.Width - padding, talentFrameBackground.Rect.Height - padding), infoFrame.RectTransform, Anchor.Center), style: null); - GUIFrame paddedTalentFrame = new GUIFrame(new RectTransform(new Vector2(0.9f), talentFrameContent.RectTransform, Anchor.Center), style: null); + GUIFrame paddedTalentFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.9f), talentFrameContent.RectTransform, Anchor.Center), style: null); - if (controlledCharacter.Info == null) + GUIFrame talentFrameMain = new GUIFrame(new RectTransform(Vector2.One, paddedTalentFrame.RectTransform), style: null); + + GUIFrame characterSettingsFrame = null; + GUILayoutGroup characterLayout = null; + if (!(GameMain.NetworkMember is null)) + { + characterSettingsFrame = new GUIFrame(new RectTransform(Vector2.One, talentFrameContent.RectTransform), style: null) { Visible = false }; + characterLayout = new GUILayoutGroup(new RectTransform(Vector2.One, characterSettingsFrame.RectTransform)); + GUIFrame containerFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.9f), characterLayout.RectTransform), style: null); + GUIFrame playerFrame = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.7f), containerFrame.RectTransform, Anchor.Center), style: null); + GameMain.NetLobbyScreen.CreatePlayerFrame(playerFrame, alwaysAllowEditing: true, createPendingText: false); + } + + if (controlledCharacter.Info is null) { DebugConsole.ThrowError("No character info found for talent UI"); return; @@ -1229,7 +1252,7 @@ namespace Barotrauma selectedTalents = controlledCharacter.Info.UnlockedTalents.ToList(); - GUILayoutGroup talentFrameLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), paddedTalentFrame.RectTransform, anchor: Anchor.Center), childAnchor: Anchor.TopCenter) + GUILayoutGroup talentFrameLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), talentFrameMain.RectTransform, anchor: Anchor.Center), childAnchor: Anchor.TopCenter) { AbsoluteSpacing = GUI.IntScale(5) }; @@ -1241,11 +1264,11 @@ namespace Barotrauma new GUICustomComponent(new RectTransform(new Vector2(0.25f, 1f), talentInfoLayoutGroup.RectTransform), onDraw: (batch, component) => { - float posY = component.Rect.Bottom - component.Rect.Width; + float posY = component.Rect.Center.Y - component.Rect.Width / 2; info.DrawPortrait(batch, new Vector2(component.Rect.X, posY), Vector2.Zero, component.Rect.Width, false, false); }); - GUILayoutGroup nameLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.375f, 1f), talentInfoLayoutGroup.RectTransform)); + GUILayoutGroup nameLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1f), talentInfoLayoutGroup.RectTransform)) { RelativeSpacing = 0.05f }; Vector2 nameSize = GUI.SubHeadingFont.MeasureString(info.Name); GUITextBlock nameBlock = new GUITextBlock(new RectTransform(Vector2.One, nameLayout.RectTransform), info.Name, font: GUI.SubHeadingFont) { TextColor = job.Prefab.UIColor }; @@ -1260,7 +1283,56 @@ namespace Barotrauma GUITextBlock traitBlock = new GUITextBlock(new RectTransform(Vector2.One, nameLayout.RectTransform), traitString, font: GUI.SmallFont); traitBlock.RectTransform.NonScaledSize = traitSize.Pad(traitBlock.Padding).ToPoint(); - GUILayoutGroup skillLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.375f, 1f), talentInfoLayoutGroup.RectTransform)) { Stretch = true }; + if (!(GameMain.NetworkMember is null)) + { + GUIButton newCharacterBox = new GUIButton(new RectTransform(Vector2.One, nameLayout.RectTransform, Anchor.BottomCenter), text: GameMain.NetLobbyScreen.CampaignCharacterDiscarded ? TextManager.Get("settings") : TextManager.Get("createnew")) + { + IgnoreLayoutGroups = true + }; + + newCharacterBox.OnClicked = (button, o) => + { + if (!GameMain.NetLobbyScreen.CampaignCharacterDiscarded) + { + GameMain.NetLobbyScreen.TryDiscardCampaignCharacter(() => + { + newCharacterBox.Text = TextManager.Get("settings"); + + if (pendingChangesFrame != null) + { + NetLobbyScreen.CreateChangesPendingFrame(pendingChangesFrame); + } + OpenMenu(); + }); + return true; + } + + OpenMenu(); + return true; + + void OpenMenu() + { + characterSettingsFrame!.Visible = true; + talentFrameMain.Visible = false; + } + }; + + 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")) + { + OnClicked = (button, o) => + { + characterSettingsFrame!.Visible = false; + talentFrameMain.Visible = true; + return true; + } + }; + } + } + + GUILayoutGroup skillLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.45f, 1f), talentInfoLayoutGroup.RectTransform)) { Stretch = true }; string skillString = TextManager.Get("skills"); Vector2 skillSize = GUI.SubHeadingFont.MeasureString(skillString); @@ -1276,6 +1348,7 @@ namespace Barotrauma GUIListBox talentTreeListBox = new GUIListBox(new RectTransform(new Vector2(1f, 0.7f), talentFrameLayoutGroup.RectTransform, Anchor.TopCenter), isHorizontal: true, style: null); + List subTreeNames = new List(); foreach (var subTree in talentTree.TalentSubTrees) { GUIFrame subTreeFrame = new GUIFrame(new RectTransform(new Vector2(0.333f, 1f), talentTreeListBox.Content.RectTransform, anchor: Anchor.TopLeft), style: null); @@ -1285,7 +1358,7 @@ namespace Barotrauma int elementPadding = GUI.IntScale(8); Point headerSize = subtreeTitleFrame.RectTransform.NonScaledSize; GUIFrame subTreeTitleBackground = new GUIFrame(new RectTransform(new Point(headerSize.X - elementPadding, headerSize.Y), subtreeTitleFrame.RectTransform, anchor: Anchor.Center), style: "SubtreeHeader"); - new GUITextBlock(new RectTransform(Vector2.One, subTreeTitleBackground.RectTransform, anchor: Anchor.TopCenter), subTree.DisplayName, font: GUI.LargeFont, textAlignment: Alignment.Center); + subTreeNames.Add(new GUITextBlock(new RectTransform(Vector2.One, subTreeTitleBackground.RectTransform, anchor: Anchor.TopCenter), subTree.DisplayName, font: GUI.SubHeadingFont, textAlignment: Alignment.Center)); for (int i = 0; i < 4; i++) { @@ -1296,7 +1369,7 @@ namespace Barotrauma GUIFrame talentBackground = new GUIFrame(new RectTransform(new Point(talentFrameSize.X - elementPadding, talentFrameSize.Y - elementPadding), talentOptionFrame.RectTransform, anchor: Anchor.Center), style: "TalentBackground"); GUIFrame talentBackgroundHighlight = new GUIFrame(new RectTransform(Vector2.One, talentBackground.RectTransform, anchor: Anchor.Center), style: "TalentBackgroundGlow") { Visible = false }; - GUIImage cornerIcon = new GUIImage(new RectTransform(new Vector2(0.25f), talentOptionFrame.RectTransform, anchor: Anchor.BottomRight, scaleBasis: ScaleBasis.BothHeight), style: null) + GUIImage cornerIcon = new GUIImage(new RectTransform(new Vector2(0.2f), talentOptionFrame.RectTransform, anchor: Anchor.BottomRight, scaleBasis: ScaleBasis.BothHeight) { MaxSize = new Point(16) }, style: null) { CanBeFocused = false }; @@ -1316,10 +1389,12 @@ namespace Barotrauma { GUIFrame talentFrame = new GUIFrame(new RectTransform(Vector2.One, talentOptionLayoutGroup.RectTransform), style: null) { - CanBeFocused = false, + CanBeFocused = false }; - GUIButton talentButton = new GUIButton(new RectTransform(Vector2.One, talentFrame.RectTransform, anchor: Anchor.Center), style: null) + GUIFrame croppedTalentFrame = new GUIFrame(new RectTransform(Vector2.One, talentFrame.RectTransform, anchor: Anchor.Center, scaleBasis: ScaleBasis.BothHeight), style: null); + + GUIButton talentButton = new GUIButton(new RectTransform(Vector2.One, croppedTalentFrame.RectTransform, anchor: Anchor.Center), style: null) { ToolTip = $"{talent.DisplayName}\n\n{talent.Description}", UserData = talent.Identifier, @@ -1361,7 +1436,7 @@ namespace Barotrauma GUIComponent iconImage; if (talent.Icon is null) { - iconImage = new GUITextBlock(new RectTransform(Vector2.One, talentButton.RectTransform, anchor: Anchor.Center, scaleBasis: ScaleBasis.BothHeight), text: "???", font: GUI.LargeFont, textAlignment: Alignment.Center, style: null) + iconImage = new GUITextBlock(new RectTransform(Vector2.One, talentButton.RectTransform, anchor: Anchor.Center), text: "???", font: GUI.LargeFont, textAlignment: Alignment.Center, style: null) { OutlineColor = GUI.Style.Red, TextColor = GUI.Style.Red, @@ -1387,10 +1462,11 @@ namespace Barotrauma } } } + GUITextBlock.AutoScaleAndNormalize(subTreeNames); GUILayoutGroup talentBottomFrame = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.07f), talentFrameLayoutGroup.RectTransform, Anchor.TopCenter), isHorizontal: true) { RelativeSpacing = 0.01f }; - GUILayoutGroup experienceLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.775f, 1f), talentBottomFrame.RectTransform)); + GUILayoutGroup experienceLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.59f, 1f), talentBottomFrame.RectTransform)); GUIFrame experienceBarFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.5f), experienceLayout.RectTransform), style: null); experienceBar = new GUIProgressBar(new RectTransform(new Vector2(1f, 1f), experienceBarFrame.RectTransform, Anchor.CenterLeft), @@ -1399,19 +1475,22 @@ namespace Barotrauma IsHorizontal = true }; - experienceText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), experienceBarFrame.RectTransform, anchor: Anchor.Center), "", font: GUI.Font, textAlignment: Alignment.CenterRight); - - talentPointText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), experienceLayout.RectTransform, anchor: Anchor.Center), "", font: GUI.SubHeadingFont, parseRichText: true, textAlignment: Alignment.CenterRight); - - new GUIButton(new RectTransform(new Vector2(0.1f, 1f), talentBottomFrame.RectTransform), text: TextManager.Get("reset"), style: "GUICharacterInfoButton") + experienceText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), experienceBarFrame.RectTransform, anchor: Anchor.Center), "", font: GUI.Font, textAlignment: Alignment.CenterRight) { - OnClicked = ResetTalentSelection, + Shadow = true }; - new GUIButton(new RectTransform(new Vector2(0.1f, 1f), talentBottomFrame.RectTransform), text: TextManager.Get("applysettingsbutton"), style: "GUICharacterInfoButton") + talentPointText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), experienceLayout.RectTransform, anchor: Anchor.Center), "", font: GUI.SubHeadingFont, parseRichText: true, textAlignment: Alignment.CenterRight) { AutoScaleVertical = true }; + + talentResetButton = new GUIButton(new RectTransform(new Vector2(0.19f, 1f), talentBottomFrame.RectTransform), text: TextManager.Get("reset"), style: "GUIButtonFreeScale") + { + OnClicked = ResetTalentSelection + }; + talentApplyButton = new GUIButton(new RectTransform(new Vector2(0.19f, 1f), talentBottomFrame.RectTransform), text: TextManager.Get("applysettingsbutton"), style: "GUIButtonFreeScale") { OnClicked = ApplyTalentSelection, }; + GUITextBlock.AutoScaleAndNormalize(talentResetButton.TextBlock, talentApplyButton.TextBlock); UpdateTalentButtons(); } @@ -1419,11 +1498,12 @@ namespace Barotrauma private void CreateTalentSkillList(Character character, GUIListBox parent) { parent.Content.ClearChildren(); + List skillNames = new List(); foreach (Skill skill in character.Info.Job.Skills) { GUILayoutGroup skillContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.2f), parent.Content.RectTransform), isHorizontal: true) { CanBeFocused = false }; - new GUITextBlock(new RectTransform(new Vector2(0.7f, 1f), skillContainer.RectTransform), TextManager.Get($"skillname.{skill.Identifier}", returnNull: true) ?? skill.Identifier); + skillNames.Add(new GUITextBlock(new RectTransform(new Vector2(0.7f, 1f), skillContainer.RectTransform), TextManager.Get($"skillname.{skill.Identifier}", returnNull: true) ?? skill.Identifier)); new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), skillContainer.RectTransform), Math.Floor(skill.Level).ToString("F0"), textAlignment: Alignment.CenterRight) { Padding = new Vector4(0, 0, 4, 0) }; float modifiedSkillLevel = character.GetSkillLevel(skill.Identifier); @@ -1444,6 +1524,7 @@ namespace Barotrauma } parent.RecalculateChildren(); + GUITextBlock.AutoScaleAndNormalize(skillNames); } private void UpdateTalentButtons() diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index e979ecbeb..ad9693350 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -1068,7 +1068,6 @@ namespace Barotrauma spriteBatch.End(); } - sw.Stop(); PerformanceCounter.AddElapsedTicks("Draw total", sw.ElapsedTicks); PerformanceCounter.DrawTimeGraph.Update(sw.ElapsedTicks * 1000.0f / (float)Stopwatch.Frequency); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index 99526244b..32605d848 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -60,7 +60,7 @@ namespace Barotrauma var newCampaignContainer = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.95f), campaignContainer.RectTransform, Anchor.Center), style: null); var loadCampaignContainer = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.95f), campaignContainer.RectTransform, Anchor.Center), style: null); - GameMain.NetLobbyScreen.CampaignSetupUI = new CampaignSetupUI(true, newCampaignContainer, loadCampaignContainer, null, saveFiles); + GameMain.NetLobbyScreen.CampaignSetupUI = new MultiPlayerCampaignSetupUI(newCampaignContainer, loadCampaignContainer, null, saveFiles); var newCampaignButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), buttonContainer.RectTransform), TextManager.Get("NewCampaign"), style: "GUITabButton") diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs index 86db15f21..b5246980c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs @@ -21,7 +21,8 @@ namespace Barotrauma { if (GameMain.NetworkMember != null && GameMain.NetLobbyScreen != null) { - if (GameMain.NetLobbyScreen.HeadSelectionList != null) { GameMain.NetLobbyScreen.HeadSelectionList.Visible = false; } + GameMain.NetLobbyScreen.CharacterAppearanceCustomizationMenu?.Dispose(); + GameMain.NetLobbyScreen.CharacterAppearanceCustomizationMenu = null; if (GameMain.NetLobbyScreen.JobSelectionFrame != null) { GameMain.NetLobbyScreen.JobSelectionFrame.Visible = false; } } if (tabMenu == null && !(GameMode is TutorialMode) && !ConversationAction.IsDialogOpen) @@ -39,6 +40,7 @@ namespace Barotrauma private GUILayoutGroup topLeftButtonGroup; private GUIButton crewListButton, commandButton, tabMenuButton; + private GUIImage talentPointNotification; private GUIComponent respawnInfoFrame, respawnButtonContainer; private GUITextBlock respawnInfoText; @@ -88,11 +90,11 @@ namespace Barotrauma tabMenuButton = new GUIButton(new RectTransform(buttonSize, parent: topLeftButtonGroup.RectTransform), style: "TabMenuButton") { ToolTip = TextManager.GetWithVariable("hudbutton.tabmenu", "[key]", GameMain.Config.KeyBindText(InputType.InfoTab)), - OnClicked = (button, userData) => - { - return ToggleTabMenu(); - } + OnClicked = (button, userData) => ToggleTabMenu() }; + + talentPointNotification = CreateTalentIconNotification(tabMenuButton); + GameMain.Instance.ResolutionChanged += CreateTopLeftButtons; respawnInfoFrame = new GUIFrame(new RectTransform(new Vector2(0.5f, 1.0f), parent: topLeftButtonGroup.RectTransform) @@ -139,11 +141,41 @@ namespace Barotrauma if (GameMain.NetworkMember != null) { - GameMain.NetLobbyScreen?.HeadSelectionList?.AddToGUIUpdateList(); + GameMain.NetLobbyScreen.CharacterAppearanceCustomizationMenu?.AddToGUIUpdateList(); GameMain.NetLobbyScreen?.JobSelectionFrame?.AddToGUIUpdateList(); } } + public static GUIImage CreateTalentIconNotification(GUIComponent parent, bool offset = true) + { + GUIImage indicator = new GUIImage(new RectTransform(new Vector2(0.45f), parent.RectTransform, anchor: Anchor.TopRight, scaleBasis: ScaleBasis.BothWidth), style: "TalentPointNotification") + { + Visible = false, + CanBeFocused = false + }; + Point notificationSize = indicator.RectTransform.NonScaledSize; + if (offset) + { + indicator.RectTransform.AbsoluteOffset = new Point(-(notificationSize.X / 2), -(notificationSize.Y / 2)); + } + return indicator; + } + + public static void UpdateTalentNotificationIndicator(GUIImage indicator) + { + if (indicator != null) + { + if (Character.Controlled?.Info == null) + { + indicator.Visible = false; + } + else + { + indicator.Visible = Character.Controlled.Info.GetAvailableTalentPoints() > 0; + } + } + } + partial void UpdateProjSpecific(float deltaTime) { if (GUI.DisableHUD) { return; } @@ -158,28 +190,24 @@ namespace Barotrauma else { tabMenu.Update(); - if ((PlayerInput.KeyHit(InputType.InfoTab) || PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.Escape)) && + if ((PlayerInput.KeyHit(InputType.InfoTab) || PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.Escape)) && !(GUI.KeyboardDispatcher.Subscriber is GUITextBox)) { ToggleTabMenu(); } } + UpdateTalentNotificationIndicator(talentPointNotification); + if (GameMain.NetworkMember != null) { - if (GameMain.NetLobbyScreen?.HeadSelectionList != null) - { - if (PlayerInput.PrimaryMouseButtonDown() && !GUI.IsMouseOn(GameMain.NetLobbyScreen.HeadSelectionList)) - { - if (GameMain.NetLobbyScreen.HeadSelectionList != null) { GameMain.NetLobbyScreen.HeadSelectionList.Visible = false; } - } - } + GameMain.NetLobbyScreen?.CharacterAppearanceCustomizationMenu?.Update(); if (GameMain.NetLobbyScreen?.JobSelectionFrame != null) { - if (PlayerInput.PrimaryMouseButtonDown() && !GUI.IsMouseOn(GameMain.NetLobbyScreen.JobSelectionFrame)) + if (GameMain.NetLobbyScreen.JobSelectionFrame != null && PlayerInput.PrimaryMouseButtonDown() && !GUI.IsMouseOn(GameMain.NetLobbyScreen.JobSelectionFrame)) { GameMain.NetLobbyScreen.JobList.Deselect(); - if (GameMain.NetLobbyScreen.JobSelectionFrame != null) { GameMain.NetLobbyScreen.JobSelectionFrame.Visible = false; } + GameMain.NetLobbyScreen.JobSelectionFrame.Visible = false; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index 72434637b..2e3782d15 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -323,7 +323,7 @@ namespace Barotrauma case Layout.Default: { int personalSlotCount = SlotTypes.Count(s => PersonalSlots.HasFlag(s)); - int normalSlotCount = SlotTypes.Count(s => !PersonalSlots.HasFlag(s)); + int normalSlotCount = SlotTypes.Count(s => !PersonalSlots.HasFlag(s) && s != InvSlotType.HealthInterface); int x = GameMain.GraphicsWidth / 2 - normalSlotCount * (SlotSize.X + Spacing) / 2; int upperX = HUDLayoutSettings.BottomRightInfoArea.X - SlotSize.X - Spacing * 4 - HideButtonWidth; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/GeneticMaterial.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/GeneticMaterial.cs index 37c6d9553..174fba2b0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/GeneticMaterial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/GeneticMaterial.cs @@ -14,7 +14,7 @@ namespace Barotrauma.Items.Components public override void AddTooltipInfo(ref string name, ref string description) { - if (!string.IsNullOrEmpty(materialName)) + if (!string.IsNullOrEmpty(materialName) && item.ContainedItems.Count() > 0) { string mergedMaterialName = materialName; foreach (Item containedItem in item.ContainedItems) @@ -23,7 +23,7 @@ namespace Barotrauma.Items.Components if (containedMaterial == null) { continue; } mergedMaterialName += ", " + containedMaterial.materialName; } - name = TextManager.GetWithVariable("entityname.geneticmaterial", "[type]", mergedMaterialName); + name = name.Replace(materialName, mergedMaterialName); } if (Tainted) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs index cf62a7d76..6e6fbe63e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs @@ -164,7 +164,7 @@ namespace Barotrauma.Items.Components } } } - activateButton.Enabled = inputContainer.Inventory.AllItems.Any(); + activateButton.Enabled = outputsFound; activateButton.Text = TextManager.Get(ActivateButtonText); }; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index fb9f701ea..fabc40b20 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -516,11 +516,7 @@ namespace Barotrauma.Items.Components string name = GetRecipeNameAndAmount(selectedItem); - float quality = 0; - foreach (string tag in selectedItem.TargetItem.Tags) - { - quality += user?.Info?.GetSavedStatValue(StatTypes.IncreaseFabricationQuality, tag) ?? 0; - } + float quality = GetFabricatedItemQuality(selectedItem, user); if (quality > 0) { name = TextManager.GetWithVariable("itemname.quality" + (int)quality, "[itemname]", name+'\n', fallBackTag: "itemname.quality3"); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index 0c213a8cb..59c300a13 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -246,25 +246,26 @@ namespace Barotrauma.Items.Components protected override void CreateGUI() { + GuiFrame.ClearChildren(); + GuiFrame.RectTransform.RelativeOffset = new Vector2(0.05f, 0.0f); GuiFrame.CanBeFocused = true; - new GUICustomComponent(new RectTransform(GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin, GuiFrame.RectTransform, Anchor.Center) { AbsoluteOffset = GUIStyle.ItemFrameOffset }, DrawHUDBack, null); - GUIFrame paddedContainer = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.9f), GuiFrame.RectTransform, Anchor.Center), style: null); + var submarineBack = new GUICustomComponent(new RectTransform(GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin, GuiFrame.RectTransform, Anchor.Center) { AbsoluteOffset = GUIStyle.ItemFrameOffset }, DrawHUDBack, null); + GUIFrame paddedContainer = new GUIFrame(new RectTransform(GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin, GuiFrame.RectTransform, Anchor.Center), style: null); submarineContainer = new GUIFrame(new RectTransform(Vector2.One, paddedContainer.RectTransform, Anchor.Center), style: null); - - new GUICustomComponent(new RectTransform(GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin, GuiFrame.RectTransform, Anchor.Center) { AbsoluteOffset = GUIStyle.ItemFrameOffset }, DrawHUDFront, null) + var submarineFront = new GUICustomComponent(new RectTransform(GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin, GuiFrame.RectTransform, Anchor.Center) { AbsoluteOffset = GUIStyle.ItemFrameOffset }, DrawHUDFront, null) { CanBeFocused = false }; - GUILayoutGroup buttonLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 0.2f), paddedContainer.RectTransform), isHorizontal: true); + GUILayoutGroup buttonLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 0.15f), paddedContainer.RectTransform) { MaxSize = new Point(int.MaxValue, GUI.IntScale(40)) }, isHorizontal: true) { CanBeFocused = true }; modeSwitchButtons = ImmutableArray.Create ( - new GUIButton(new RectTransform(new Vector2(0.25f, 0.5f), buttonLayout.RectTransform), string.Empty, style: "StatusMonitorButton.HullStatus") { UserData = MiniMapMode.HullStatus, Enabled = EnableHullStatus, ToolTip = TextManager.Get("StatusMonitorButton.HullStatus.Tooltip") }, - new GUIButton(new RectTransform(new Vector2(0.25f, 0.5f), buttonLayout.RectTransform), string.Empty, style: "StatusMonitorButton.ElectricalView") { UserData = MiniMapMode.ElectricalView, Enabled = EnableHullCondition, ToolTip = TextManager.Get("StatusMonitorButton.ElectricalView.Tooltip") }, - new GUIButton(new RectTransform(new Vector2(0.25f, 0.5f), buttonLayout.RectTransform), string.Empty, style: "StatusMonitorButton.HullCondition") { UserData = MiniMapMode.HullCondition, Enabled = EnableHullCondition, ToolTip = TextManager.Get("StatusMonitorButton.HullCondition.Tooltip") }, - new GUIButton(new RectTransform(new Vector2(0.25f, 0.5f), buttonLayout.RectTransform), string.Empty, style: "StatusMonitorButton.ItemFinder") { UserData = MiniMapMode.ItemFinder, Enabled = EnableItemFinder, ToolTip = TextManager.Get("StatusMonitorButton.ItemFinder.Tooltip") } + new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), buttonLayout.RectTransform), string.Empty, style: "StatusMonitorButton.HullStatus") { UserData = MiniMapMode.HullStatus, Enabled = EnableHullStatus, ToolTip = TextManager.Get("StatusMonitorButton.HullStatus.Tooltip") }, + new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), buttonLayout.RectTransform), string.Empty, style: "StatusMonitorButton.ElectricalView") { UserData = MiniMapMode.ElectricalView, Enabled = EnableHullCondition, ToolTip = TextManager.Get("StatusMonitorButton.ElectricalView.Tooltip") }, + new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), buttonLayout.RectTransform), string.Empty, style: "StatusMonitorButton.HullCondition") { UserData = MiniMapMode.HullCondition, Enabled = EnableHullCondition, ToolTip = TextManager.Get("StatusMonitorButton.HullCondition.Tooltip") }, + new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), buttonLayout.RectTransform), string.Empty, style: "StatusMonitorButton.ItemFinder") { UserData = MiniMapMode.ItemFinder, Enabled = EnableItemFinder, ToolTip = TextManager.Get("StatusMonitorButton.ItemFinder.Tooltip") } ); foreach (GUIButton button in modeSwitchButtons) @@ -295,14 +296,15 @@ namespace Barotrauma.Items.Components List reports = Order.PrefabList.FindAll(o => o.IsReport && o.SymbolSprite != null && !o.Hidden); - GUIFrame bottomFrame = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.15f), paddedContainer.RectTransform, Anchor.BottomCenter), style: null) + GUIFrame bottomFrame = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.15f), paddedContainer.RectTransform, Anchor.BottomCenter) { MaxSize = new Point(int.MaxValue, GUI.IntScale(40)) }, style: null) { CanBeFocused = false }; reportFrame = new GUILayoutGroup(new RectTransform(new Vector2(1), bottomFrame.RectTransform), isHorizontal: true) { - AbsoluteSpacing = (int)(5 * GUI.Scale) + Stretch = true, + AbsoluteSpacing = GUI.IntScale(5) }; if (reports.Any()) @@ -359,10 +361,7 @@ namespace Barotrauma.Items.Components searchBar.OnSelected += (sender, key) => { - itemsFoundOnSub = Item.ItemList.Where(it => - it.Submarine == item.Submarine && - !it.NonInteractable && !it.HiddenInGame && - (it.GetComponent() != null || it.GetComponent() != null)).Select(it => it.Prefab).ToImmutableHashSet(); + itemsFoundOnSub = Item.ItemList.Where(it => VisibleOnItemFinder(it)).Select(it => it.Prefab).ToImmutableHashSet(); }; searchBar.OnKeyHit += ControlSearchTooltip; @@ -390,6 +389,28 @@ namespace Barotrauma.Items.Components c.CanBeFocused = false; c.Children.ForEach(c2 => c2.CanBeFocused = false); }); + + submarineBack.RectTransform.MaxSize = + submarineFront.RectTransform.MaxSize = + submarineContainer.RectTransform.MaxSize = + new Point(int.MaxValue, paddedContainer.Rect.Height - bottomFrame.Rect.Height - buttonLayout.Rect.Height); + } + + private bool VisibleOnItemFinder(Item it) + { + if (it.Submarine != item.Submarine) { return false; } + if (it.NonInteractable || it.HiddenInGame) { return false; } + if (it.GetComponent() == null) { return false; } + + var holdable = it.GetComponent(); + if (holdable != null && holdable.Attached) { return false; } + + var wire = it.GetComponent(); + if (wire != null && wire.Connections.Any(c => c != null)) { return false; } + + if (it.HasTag("traitormissionitem")) { return false; } + + return true; } public override void AddToGUIUpdateList() @@ -546,10 +567,7 @@ namespace Barotrauma.Items.Components // is there a better way to do this? if (GuiFrame.Rect.Size != elementSize) { - if (item.Submarine is { } sub) - { - BakeSubmarine(sub, miniMapFrame.Rect); - } + CreateGUI(); elementSize = GuiFrame.Rect.Size; } @@ -782,10 +800,7 @@ namespace Barotrauma.Items.Components foreach (Item it in Item.ItemList) { - if (it.Submarine != item.Submarine) { continue; } - if (it.HiddenInGame || it.NonInteractable) { continue; } - if (it.GetComponent() is { Connections: { } conn} && conn.Any()) { continue; } - if (it.HasTag("traitormissionitem")) { continue; } + if (!VisibleOnItemFinder(it)) { continue; } if (it.Prefab == searchedPrefab) { @@ -794,7 +809,7 @@ namespace Barotrauma.Items.Components if (it.FindParentInventory(inventory => inventory is ItemInventory { Owner: Item { ParentInventory: null } }) is ItemInventory parent) { - foundItems.Add((Item) parent.Owner); + foundItems.Add((Item)parent.Owner); } else { @@ -1165,7 +1180,7 @@ namespace Barotrauma.Items.Components if (entity is Item it) { - if (it.GetComponent() != null || it.ParentInventory != null) { continue; } + if (it.GetComponent() != null || it.ParentInventory != null) { continue; } DrawItem(spriteBatch, it, parentRect, worldBorders, inflate); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Quality.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Quality.cs new file mode 100644 index 000000000..8c17e9d90 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Quality.cs @@ -0,0 +1,21 @@ +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Barotrauma.Items.Components +{ + partial class Quality : ItemComponent + { + public override void AddTooltipInfo(ref string name, ref string description) + { + foreach (var statValue in statValues) + { + int roundedValue = (int)Math.Round(statValue.Value * qualityLevel * 100); + if (roundedValue == 0) { return; } + string colorStr = XMLExtensions.ColorToString(GUI.Style.Green); + description += $"\n ‖color:{colorStr}‖{roundedValue.ToString("+0;-#")}%‖color:end‖ {TextManager.Get("qualitystattypenames." + statValue.Key.ToString(), true) ?? statValue.Key.ToString()}"; + } + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs index f68bdc796..fcdf7f34c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs @@ -51,7 +51,7 @@ namespace Barotrauma.Items.Components public override bool ShouldDrawHUD(Character character) { if (!HasRequiredItems(character, false) || character.SelectedConstruction != item) return false; - return item.ConditionPercentage < RepairThreshold || character.IsTraitor && item.ConditionPercentage > MinSabotageCondition || (CurrentFixer == character && (!item.IsFullCondition || (character.IsTraitor && item.ConditionPercentage > MinSabotageCondition))) || CanTinker(character); + return item.ConditionPercentage < RepairThreshold || character.IsTraitor && item.ConditionPercentage > MinSabotageCondition || (CurrentFixer == character && (!item.IsFullCondition || (character.IsTraitor && item.ConditionPercentage > MinSabotageCondition))) || IsTinkerable(character); } partial void InitProjSpecific(XElement element) @@ -162,7 +162,7 @@ namespace Barotrauma.Items.Components tinkerButtonText = "Tinker"; tinkeringText = "Tinkering"; - TinkerButton = new GUIButton(new RectTransform(new Vector2(0.8f, 0.15f), paddedFrame.RectTransform, Anchor.BottomCenter), tinkerButtonText, style: "GUIButtonSmall") + TinkerButton = new GUIButton(new RectTransform(new Vector2(0.8f, 0.15f), paddedFrame.RectTransform, Anchor.BottomCenter), tinkerButtonText) { IgnoreLayoutGroups = true, Visible = false, @@ -254,7 +254,7 @@ namespace Barotrauma.Items.Components progressBarOverlayText.Visible = false; } - RepairButton.Enabled = (currentFixerAction == FixActions.None || (CurrentFixer == character && currentFixerAction != FixActions.Repair)) && !item.IsFullCondition; + RepairButton.Enabled = (currentFixerAction == FixActions.None || (CurrentFixer == character && currentFixerAction != FixActions.Repair)) && !item.IsFullCondition && item.ConditionPercentage < RepairThreshold; RepairButton.Text = (currentFixerAction == FixActions.None || CurrentFixer != character || currentFixerAction != FixActions.Repair) ? repairButtonText : repairingText + new string('.', ((int)(Timing.TotalTime * 2.0f) % 3) + 1); @@ -269,7 +269,7 @@ namespace Barotrauma.Items.Components TinkerButton.Visible = IsTinkerable(character); TinkerButton.IgnoreLayoutGroups = !TinkerButton.Visible; TinkerButton.Enabled = (currentFixerAction == FixActions.None || (CurrentFixer == character && currentFixerAction != FixActions.Tinker)) && CanTinker(character); - TinkerButton.Text = (currentFixerAction == FixActions.None || CurrentFixer != character || currentFixerAction != FixActions.Tinker && CanTinker(character)) ? + TinkerButton.Text = (currentFixerAction == FixActions.None || CurrentFixer != character || currentFixerAction != FixActions.Tinker) ? tinkerButtonText : tinkeringText + new string('.', ((int)(Timing.TotalTime * 2.0f) % 3) + 1); @@ -326,6 +326,7 @@ namespace Barotrauma.Items.Components deteriorateAlwaysResetTimer = msg.ReadSingle(); DeteriorateAlways = msg.ReadBoolean(); tinkeringDuration = msg.ReadSingle(); + tinkeringStrength = msg.ReadSingle(); ushort currentFixerID = msg.ReadUInt16(); currentFixerAction = (FixActions)msg.ReadRangedInteger(0, 2); CurrentFixer = currentFixerID != 0 ? Entity.FindEntityByID(currentFixerID) as Character : null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index daa10c434..d2a1e999e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -316,11 +316,12 @@ namespace Barotrauma string colorStr = XMLExtensions.ColorToString(!item.AllowStealing ? GUI.Style.Red : Color.White); + toolTip = $"‖color:{colorStr}‖{name}‖color:end‖"; if (item.Quality > 0) { - name = TextManager.GetWithVariable("itemname.quality" + item.Quality, "[itemname]", name, fallBackTag: "itemname.quality3"); + // substring by to get rid of the empty space at start, text file should be adjusted + toolTip += $"\n{TextManager.GetWithVariable("itemname.quality" + item.Quality, "[itemname]", "", fallBackTag: "itemname.quality3")?.Substring(1)}"; } - toolTip = $"‖color:{colorStr}‖{name}‖color:end‖"; if (itemsInSlot.All(it => it.NonInteractable || it.NonPlayerTeamInteractable)) { @@ -1653,9 +1654,9 @@ namespace Barotrauma scale: iconSize.X / stealIcon.size.X); } int maxStackSize = item.Prefab.MaxStackSize; - if (item.Container != null) + if (inventory is ItemInventory itemInventory) { - maxStackSize = Math.Min(maxStackSize, item.Container.GetComponent()?.GetMaxStackSize(slotIndex) ?? maxStackSize); + maxStackSize = Math.Min(maxStackSize, itemInventory.Container.GetMaxStackSize(slotIndex)); } if (maxStackSize > 1 && inventory != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs index 7a29743ad..91eb1ce2a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs @@ -265,7 +265,7 @@ namespace Barotrauma if (!currentDisplayLocation.Discovered) { RemoveFogOfWar(currentDisplayLocation); - currentDisplayLocation.Discovered = true; + currentDisplayLocation.Discover(); if (currentDisplayLocation.MapPosition.X > furthestDiscoveredLocation.MapPosition.X) { furthestDiscoveredLocation = currentDisplayLocation; @@ -426,7 +426,7 @@ namespace Barotrauma Level.Loaded.DebugSetStartLocation(CurrentLocation); Level.Loaded.DebugSetEndLocation(null); - CurrentLocation.Discovered = true; + CurrentLocation.Discover(); OnLocationChanged?.Invoke(prevLocation, CurrentLocation); SelectLocation(-1); if (GameMain.Client == null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index bc8cc1075..8dd2d4939 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -2755,6 +2755,9 @@ namespace Barotrauma.Networking msg.Write((byte)characterInfo.BeardIndex); msg.Write((byte)characterInfo.MoustacheIndex); msg.Write((byte)characterInfo.FaceAttachmentIndex); + msg.WriteColorR8G8B8(characterInfo.SkinColor); + msg.WriteColorR8G8B8(characterInfo.HairColor); + msg.WriteColorR8G8B8(characterInfo.FacialHairColor); var jobPreferences = GameMain.NetLobbyScreen.JobPreferences; int count = Math.Min(jobPreferences.Count, 3); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs new file mode 100644 index 000000000..81ead93e1 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/CampaignSetupUI.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; + +namespace Barotrauma +{ + abstract class CampaignSetupUI + { + protected readonly GUIComponent newGameContainer, loadGameContainer; + + protected GUIListBox subList; + protected GUIListBox saveList; + protected List subTickBoxes; + + protected GUITextBox saveNameBox, seedBox; + + protected GUILayoutGroup subPreviewContainer; + + protected GUIButton loadGameButton; + + public Action StartNewGame; + public Action LoadGame; + + protected enum CategoryFilter { All = 0, Vanilla = 1, Custom = 2 }; + protected CategoryFilter subFilter = CategoryFilter.All; + + public GUIButton StartButton + { + get; + protected set; + } + + public GUITextBlock InitialMoneyText + { + get; + protected set; + } + + public GUITickBox EnableRadiationToggle { get; set; } + public GUILayoutGroup CampaignSettingsContent { get; set; } + + public GUIButton CampaignCustomizeButton { get; set; } + public GUIMessageBox CampaignCustomizeSettings { get; set; } + + public GUITextBlock MaxMissionCountText; + + public CampaignSetupUI(GUIComponent newGameContainer, GUIComponent loadGameContainer) + { + this.newGameContainer = newGameContainer; + this.loadGameContainer = loadGameContainer; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs new file mode 100644 index 000000000..e02424ee4 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs @@ -0,0 +1,521 @@ +using Barotrauma.Tutorials; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using Barotrauma.IO; +using System.Linq; +using System.Xml.Linq; +using System.Globalization; +using Barotrauma.Extensions; + +namespace Barotrauma +{ + class MultiPlayerCampaignSetupUI : CampaignSetupUI + { + private GUIButton deleteMpSaveButton; + + public MultiPlayerCampaignSetupUI(GUIComponent newGameContainer, GUIComponent loadGameContainer, IEnumerable submarines, IEnumerable saveFiles = null) + : base(newGameContainer, loadGameContainer) + { + var columnContainer = new GUILayoutGroup(new RectTransform(Vector2.One, newGameContainer.RectTransform), isHorizontal: true) + { + Stretch = true, + RelativeSpacing = 0.0f + }; + + var leftColumn = new GUILayoutGroup(new RectTransform(Vector2.One, columnContainer.RectTransform)) + { + Stretch = true, + RelativeSpacing = 0.015f + }; + + var rightColumn = new GUILayoutGroup(new RectTransform(Vector2.Zero, columnContainer.RectTransform)) + { + Stretch = true, + RelativeSpacing = 0.015f + }; + + columnContainer.Recalculate(); + + // New game left side + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.02f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("SaveName"), font: GUI.SubHeadingFont); + saveNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.05f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, string.Empty) + { + textFilterFunction = (string str) => { return ToolBox.RemoveInvalidFileNameChars(str); } + }; + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.02f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("MapSeed"), font: GUI.SubHeadingFont); + seedBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.05f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, ToolBox.RandomSeed(8)); + + // Spacing to fix the multiplayer campaign setup layout + CreateMultiplayerCampaignSubList(leftColumn.RectTransform); + + //spacing + //new GUIFrame(new RectTransform(new Vector2(1.0f, 0.25f), leftColumn.RectTransform), style: null); + + // New game right side + subPreviewContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), rightColumn.RectTransform)) + { + Stretch = true + }; + + var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.12f), + leftColumn.RectTransform) { MaxSize = new Point(int.MaxValue, 60) }, childAnchor: Anchor.BottomRight, isHorizontal: true); + + StartButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1f), buttonContainer.RectTransform, Anchor.BottomRight) { MaxSize = new Point(350, 60) }, TextManager.Get("StartCampaignButton")) + { + OnClicked = (GUIButton btn, object userData) => + { + if (string.IsNullOrWhiteSpace(saveNameBox.Text)) + { + saveNameBox.Flash(GUI.Style.Red); + return false; + } + + SubmarineInfo selectedSub = null; + + if (GameMain.NetLobbyScreen.SelectedSub == null) { return false; } + selectedSub = GameMain.NetLobbyScreen.SelectedSub; + + if (selectedSub.SubmarineClass == SubmarineClass.Undefined) + { + new GUIMessageBox(TextManager.Get("error"), TextManager.Get("undefinedsubmarineselected")); + return false; + } + + if (string.IsNullOrEmpty(selectedSub.MD5Hash.Hash)) + { + ((GUITextBlock)subList.SelectedComponent).TextColor = Color.DarkRed * 0.8f; + subList.SelectedComponent.CanBeFocused = false; + subList.Deselect(); + return false; + } + + string savePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Multiplayer, saveNameBox.Text); + bool hasRequiredContentPackages = selectedSub.RequiredContentPackagesInstalled; + + CampaignSettings settings = new CampaignSettings(); + + settings.RadiationEnabled = GameMain.NetLobbyScreen.IsRadiationEnabled(); + settings.MaxMissionCount = GameMain.NetLobbyScreen.GetMaxMissionCount(); + + if (selectedSub.HasTag(SubmarineTag.Shuttle) || !hasRequiredContentPackages) + { + if (!hasRequiredContentPackages) + { + var msgBox = new GUIMessageBox(TextManager.Get("ContentPackageMismatch"), + TextManager.GetWithVariable("ContentPackageMismatchWarning", "[requiredcontentpackages]", string.Join(", ", selectedSub.RequiredContentPackages)), + new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); + + msgBox.Buttons[0].OnClicked = msgBox.Close; + msgBox.Buttons[0].OnClicked += (button, obj) => + { + if (GUIMessageBox.MessageBoxes.Count == 0) + { + StartNewGame?.Invoke(selectedSub, savePath, seedBox.Text, settings); + CoroutineManager.StartCoroutine(WaitForCampaignSetup(), "WaitForCampaignSetup"); + } + return true; + }; + + msgBox.Buttons[1].OnClicked = msgBox.Close; + } + + if (selectedSub.HasTag(SubmarineTag.Shuttle)) + { + var msgBox = new GUIMessageBox(TextManager.Get("ShuttleSelected"), + TextManager.Get("ShuttleWarning"), + new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); + + msgBox.Buttons[0].OnClicked = (button, obj) => + { + StartNewGame?.Invoke(selectedSub, savePath, seedBox.Text, settings); + CoroutineManager.StartCoroutine(WaitForCampaignSetup(), "WaitForCampaignSetup"); + return true; + }; + msgBox.Buttons[0].OnClicked += msgBox.Close; + + msgBox.Buttons[1].OnClicked = msgBox.Close; + return false; + } + } + else + { + StartNewGame?.Invoke(selectedSub, savePath, seedBox.Text, settings); + CoroutineManager.StartCoroutine(WaitForCampaignSetup(), "WaitForCampaignSetup"); + } + + return true; + } + }; + + InitialMoneyText = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1f), buttonContainer.RectTransform), "", font: GUI.Style.SmallFont, textColor: GUI.Style.Green) + { + TextGetter = () => + { + int initialMoney = CampaignMode.InitialMoney; + if (GameMain.NetLobbyScreen.SelectedSub != null) + { + initialMoney -= GameMain.NetLobbyScreen.SelectedSub.Price; + } + initialMoney = Math.Max(initialMoney, MultiPlayerCampaign.MinimumInitialMoney); + return TextManager.GetWithVariable("campaignstartingmoney", "[money]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", initialMoney)); + } + }; + + columnContainer.Recalculate(); + leftColumn.Recalculate(); + rightColumn.Recalculate(); + + if (submarines != null) { UpdateSubList(submarines); } + UpdateLoadMenu(saveFiles); + } + + private void CreateMultiplayerCampaignSubList(RectTransform parent) + { + GUILayoutGroup subHolder = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.725f), parent)) + { + RelativeSpacing = 0.005f, + Stretch = true + }; + + var subLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), subHolder.RectTransform) { MinSize = new Point(0, 25) }, TextManager.Get("purchasablesubmarines", fallBackTag: "workshoplabelsubmarines"), font: GUI.SubHeadingFont); + + var filterContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), subHolder.RectTransform), isHorizontal: true) + { + Stretch = true + }; + var searchTitle = new GUITextBlock(new RectTransform(new Vector2(0.001f, 1.0f), filterContainer.RectTransform), TextManager.Get("serverlog.filter"), textAlignment: Alignment.CenterLeft, font: GUI.Font); + var searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 1.0f), filterContainer.RectTransform, Anchor.CenterRight), font: GUI.Font, createClearButton: true); + filterContainer.RectTransform.MinSize = searchBox.RectTransform.MinSize; + searchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; }; + searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; + searchBox.OnTextChanged += (textBox, text) => + { + foreach (GUIComponent child in subList.Content.Children) + { + if (!(child.UserData is SubmarineInfo sub)) { continue; } + child.Visible = string.IsNullOrEmpty(text) ? true : sub.DisplayName.ToLower().Contains(text.ToLower()); + } + return true; + }; + + subList = new GUIListBox(new RectTransform(Vector2.One, subHolder.RectTransform)); + subTickBoxes = new List(); + + for (int i = 0; i < GameMain.Client.ServerSubmarines.Count; i++) + { + SubmarineInfo sub = GameMain.Client.ServerSubmarines[i]; + + if (!sub.IsCampaignCompatible) continue; + + var frame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.2f), subList.Content.RectTransform) { MinSize = new Point(0, 20) }, + style: "ListBoxElement") + { + ToolTip = sub.Description, + UserData = sub + }; + + int buttonSize = (int)(frame.Rect.Height * 0.8f); + + GUITickBox tickBox = new GUITickBox(new RectTransform(new Vector2(0.8f, 1.0f), frame.RectTransform, Anchor.CenterLeft), ToolBox.LimitString(sub.DisplayName, GUI.Font, subList.Content.Rect.Width - 65)) + { + UserData = sub, + OnSelected = (GUITickBox box) => + { + GameMain.Client.RequestCampaignSub(box.UserData as SubmarineInfo, box.Selected); + return true; + } + }; + subTickBoxes.Add(tickBox); + tickBox.Selected = GameMain.NetLobbyScreen.CampaignSubmarines.Contains(sub); + + frame.RectTransform.MinSize = new Point(0, tickBox.RectTransform.MinSize.Y); + + var subTextBlock = tickBox.TextBlock; + + var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == sub.Name && s.MD5Hash?.Hash == sub.MD5Hash?.Hash); + if (matchingSub == null) matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == sub.Name); + + if (matchingSub == null) + { + subTextBlock.TextColor = new Color(subTextBlock.TextColor, 0.5f); + frame.ToolTip = TextManager.Get("SubNotFound"); + } + else if (matchingSub?.MD5Hash == null || matchingSub.MD5Hash?.Hash != sub.MD5Hash?.Hash) + { + subTextBlock.TextColor = new Color(subTextBlock.TextColor, 0.5f); + frame.ToolTip = TextManager.Get("SubDoesntMatch"); + } + + if (!sub.RequiredContentPackagesInstalled) + { + subTextBlock.TextColor = Color.Lerp(subTextBlock.TextColor, Color.DarkRed, 0.5f); + frame.ToolTip = TextManager.Get("ContentPackageMismatch") + "\n\n" + frame.RawToolTip; + } + + var classText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), frame.RectTransform, Anchor.CenterRight), + TextManager.Get($"submarineclass.{sub.SubmarineClass}"), textAlignment: Alignment.CenterRight, font: GUI.SmallFont) + { + TextColor = subTextBlock.TextColor * 0.8f, + ToolTip = subTextBlock.RawToolTip + }; + } + } + + public void RefreshMultiplayerCampaignSubUI(List campaignSubs) + { + for (int i = 0; i < subTickBoxes.Count; i++) + { + subTickBoxes[i].Selected = campaignSubs.Contains(subTickBoxes[i].UserData as SubmarineInfo); + } + } + + private IEnumerable WaitForCampaignSetup() + { + GUI.SetCursorWaiting(); + string headerText = TextManager.Get("CampaignStartingPleaseWait"); + var msgBox = new GUIMessageBox(headerText, TextManager.Get("CampaignStarting"), new string[] { TextManager.Get("Cancel") }); + + msgBox.Buttons[0].OnClicked = (btn, userdata) => + { + GameMain.NetLobbyScreen.HighlightMode(GameMain.NetLobbyScreen.SelectedModeIndex); + GameMain.NetLobbyScreen.SelectMode(GameMain.NetLobbyScreen.SelectedModeIndex); + GUI.ClearCursorWait(); + CoroutineManager.StopCoroutines("WaitForCampaignSetup"); + return true; + }; + msgBox.Buttons[0].OnClicked += msgBox.Close; + + DateTime timeOut = DateTime.Now + new TimeSpan(0, 0, 20); + while (Screen.Selected != GameMain.GameScreen && DateTime.Now < timeOut) + { + msgBox.Header.Text = headerText + new string('.', (int)Timing.TotalTime % 3 + 1); + yield return CoroutineStatus.Running; + } + msgBox.Close(); + GUI.ClearCursorWait(); + yield return CoroutineStatus.Success; + } + + public void UpdateSubList(IEnumerable submarines) + { + List subsToShow; + string downloadFolder = Path.GetFullPath(SaveUtil.SubmarineDownloadFolder); + subsToShow = submarines.Where(s => s.IsCampaignCompatibleIgnoreClass && Path.GetDirectoryName(Path.GetFullPath(s.FilePath)) != downloadFolder).ToList(); + + subsToShow.Sort((s1, s2) => + { + int p1 = s1.Price > CampaignMode.InitialMoney ? 10 : 0; + int p2 = s2.Price > CampaignMode.InitialMoney ? 10 : 0; + return p1.CompareTo(p2) * 100 + s1.Name.CompareTo(s2.Name); + }); + + subList.ClearChildren(); + + foreach (SubmarineInfo sub in subsToShow) + { + var textBlock = new GUITextBlock( + new RectTransform(new Vector2(1, 0.1f), subList.Content.RectTransform) { MinSize = new Point(0, 30) }, + ToolBox.LimitString(sub.DisplayName, GUI.Font, subList.Rect.Width - 65), style: "ListBoxElement") + { + ToolTip = sub.Description, + UserData = sub + }; + + if (!sub.RequiredContentPackagesInstalled) + { + textBlock.TextColor = Color.Lerp(textBlock.TextColor, Color.DarkRed, .5f); + textBlock.ToolTip = TextManager.Get("ContentPackageMismatch") + "\n\n" + textBlock.RawToolTip; + } + + var priceText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), textBlock.RectTransform, Anchor.CenterRight), + TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", sub.Price)), textAlignment: Alignment.CenterRight, font: GUI.SmallFont) + { + TextColor = sub.Price > CampaignMode.InitialMoney ? GUI.Style.Red : textBlock.TextColor * 0.8f, + ToolTip = textBlock.ToolTip + }; +#if !DEBUG + if (!GameMain.DebugDraw) + { + if (sub.Price > CampaignMode.InitialMoney || !sub.IsCampaignCompatible) + { + textBlock.CanBeFocused = false; + textBlock.TextColor *= 0.5f; + } + } +#endif + } + if (SubmarineInfo.SavedSubmarines.Any()) + { + var validSubs = subsToShow.Where(s => s.IsCampaignCompatible && s.Price <= CampaignMode.InitialMoney).ToList(); + if (validSubs.Count > 0) + { + subList.Select(validSubs[Rand.Int(validSubs.Count)]); + } + } + } + + private List prevSaveFiles; + public void UpdateLoadMenu(IEnumerable saveFiles = null) + { + prevSaveFiles?.Clear(); + prevSaveFiles = null; + loadGameContainer.ClearChildren(); + + if (saveFiles == null) + { + saveFiles = SaveUtil.GetSaveFiles(SaveUtil.SaveType.Multiplayer); + } + + var leftColumn = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.85f), loadGameContainer.RectTransform), childAnchor: Anchor.TopCenter) + { + Stretch = true, + RelativeSpacing = 0.03f + }; + + saveList = new GUIListBox(new RectTransform(Vector2.One, leftColumn.RectTransform)) + { + OnSelected = SelectSaveFile + }; + + foreach (string saveFile in saveFiles) + { + string fileName = saveFile; + string subName = ""; + string saveTime = ""; + string contentPackageStr = ""; + var saveFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), saveList.Content.RectTransform) { MinSize = new Point(0, 45) }, style: "ListBoxElement") + { + UserData = saveFile + }; + + var nameText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), saveFrame.RectTransform), "") + { + CanBeFocused = false + }; + + bool isCompatible = true; + prevSaveFiles ??= new List(); + + prevSaveFiles?.Add(saveFile); + string[] splitSaveFile = saveFile.Split(';'); + saveFrame.UserData = splitSaveFile[0]; + fileName = nameText.Text = Path.GetFileNameWithoutExtension(splitSaveFile[0]); + if (splitSaveFile.Length > 1) { subName = splitSaveFile[1]; } + if (splitSaveFile.Length > 2) { saveTime = splitSaveFile[2]; } + if (splitSaveFile.Length > 3) { contentPackageStr = splitSaveFile[3]; } + + if (!string.IsNullOrEmpty(saveTime) && long.TryParse(saveTime, out long unixTime)) + { + DateTime time = ToolBox.Epoch.ToDateTime(unixTime); + saveTime = time.ToString(); + } + if (!string.IsNullOrEmpty(contentPackageStr)) + { + List contentPackagePaths = contentPackageStr.Split('|').ToList(); + if (!GameSession.IsCompatibleWithEnabledContentPackages(contentPackagePaths, out string errorMsg)) + { + nameText.TextColor = GUI.Style.Red; + saveFrame.ToolTip = string.Join("\n", errorMsg, TextManager.Get("campaignmode.contentpackagemismatchwarning")); + } + } + if (!isCompatible) + { + nameText.TextColor = GUI.Style.Red; + saveFrame.ToolTip = TextManager.Get("campaignmode.incompatiblesave"); + } + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), saveFrame.RectTransform, Anchor.BottomLeft), + text: subName, font: GUI.SmallFont) + { + CanBeFocused = false, + UserData = fileName + }; + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), saveFrame.RectTransform), + text: saveTime, textAlignment: Alignment.Right, font: GUI.SmallFont) + { + CanBeFocused = false, + UserData = fileName + }; + } + + saveList.Content.RectTransform.SortChildren((c1, c2) => + { + string file1 = c1.GUIComponent.UserData as string; + string file2 = c2.GUIComponent.UserData as string; + DateTime file1WriteTime = DateTime.MinValue; + DateTime file2WriteTime = DateTime.MinValue; + try + { + file1WriteTime = File.GetLastWriteTime(file1); + } + catch + { + //do nothing - DateTime.MinValue will be used and the element will get sorted at the bottom of the list + }; + try + { + file2WriteTime = File.GetLastWriteTime(file2); + } + catch + { + //do nothing - DateTime.MinValue will be used and the element will get sorted at the bottom of the list + }; + return file2WriteTime.CompareTo(file1WriteTime); + }); + + loadGameButton = new GUIButton(new RectTransform(new Vector2(0.45f, 0.12f), loadGameContainer.RectTransform, Anchor.BottomRight), TextManager.Get("LoadButton")) + { + OnClicked = (btn, obj) => + { + if (string.IsNullOrWhiteSpace(saveList.SelectedData as string)) { return false; } + LoadGame?.Invoke(saveList.SelectedData as string); + CoroutineManager.StartCoroutine(WaitForCampaignSetup(), "WaitForCampaignSetup"); + return true; + }, + Enabled = false + }; + deleteMpSaveButton = new GUIButton(new RectTransform(new Vector2(0.45f, 0.12f), loadGameContainer.RectTransform, Anchor.BottomLeft), + TextManager.Get("Delete"), style: "GUIButtonSmall") + { + OnClicked = DeleteSave, + Visible = false + }; + } + + private bool SelectSaveFile(GUIComponent component, object obj) + { + string fileName = (string)obj; + + loadGameButton.Enabled = true; + deleteMpSaveButton.Visible = deleteMpSaveButton.Enabled = GameMain.Client.IsServerOwner; + deleteMpSaveButton.Enabled = GameMain.GameSession?.SavePath != fileName; + if (deleteMpSaveButton.Visible) + { + deleteMpSaveButton.UserData = obj as string; + } + return true; + } + + private bool DeleteSave(GUIButton button, object obj) + { + string saveFile = obj as string; + if (obj == null) { return false; } + + string header = TextManager.Get("deletedialoglabel"); + string body = TextManager.GetWithVariable("deletedialogquestion", "[file]", Path.GetFileNameWithoutExtension(saveFile)); + + EventEditorScreen.AskForConfirmation(header, body, () => + { + SaveUtil.DeleteSave(saveFile); + prevSaveFiles?.RemoveAll(s => s.StartsWith(saveFile)); + UpdateLoadMenu(prevSaveFiles.ToList()); + return true; + }); + + return true; + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs similarity index 51% rename from Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI.cs rename to Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs index 32a158105..70dd8fbd8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/SinglePlayerCampaignSetupUI.cs @@ -10,58 +10,108 @@ using Barotrauma.Extensions; namespace Barotrauma { - class CampaignSetupUI + class SinglePlayerCampaignSetupUI : CampaignSetupUI { - private readonly GUIComponent newGameContainer, loadGameContainer; + public CharacterInfo.AppearanceCustomizationMenu[] CharacterMenus { get; private set; } - private GUIListBox subList; - private GUIListBox saveList; - private List subTickBoxes; - - private readonly GUITextBox saveNameBox, seedBox; - - private readonly GUILayoutGroup subPreviewContainer; - - private GUIButton loadGameButton, deleteMpSaveButton; - - public Action StartNewGame; - public Action LoadGame; - - private enum CategoryFilter { All = 0, Vanilla = 1, Custom = 2 }; - private CategoryFilter subFilter = CategoryFilter.All; - - public GUIButton StartButton + private GUIButton nextButton; + private GUILayoutGroup characterInfoColumns; + + public SinglePlayerCampaignSetupUI(GUIComponent newGameContainer, GUIComponent loadGameContainer, IEnumerable submarines, IEnumerable saveFiles = null) + : base(newGameContainer, loadGameContainer) { - get; - private set; + UpdateNewGameMenu(submarines); + UpdateLoadMenu(saveFiles); } - public GUITextBlock InitialMoneyText + private int currentPage = 0; + private GUIListBox pageContainer; + + public void Update() { - get; - private set; + float targetScroll = + (float)currentPage / ((float)pageContainer.Content.CountChildren - 1); + + pageContainer.BarScroll = MathHelper.Lerp(pageContainer.BarScroll, targetScroll, 0.2f); + if (MathUtils.NearlyEqual(pageContainer.BarScroll, targetScroll, 0.001f)) + { + pageContainer.BarScroll = targetScroll; + } + + for (int i=0; i + { + if (c is GUIDropDown dd) + { + dd.Dropped = false; + } + c.CanBeFocused = (i == currentPage); + }); + } + var previewListBox = subPreviewContainer.GetAllChildren().FirstOrDefault(); + previewListBox?.GetAllChildren()?.ForEach(c => + { + c.CanBeFocused = false; + }); } - public GUITickBox EnableRadiationToggle { get; set; } - public GUILayoutGroup CampaignSettingsContent { get; set; } - - public GUIButton CampaignCustomizeButton { get; set; } - public GUIMessageBox CampaignCustomizeSettings { get; set; } - - public GUITextBlock MaxMissionCountText; - - private readonly bool isMultiplayer; - - public CampaignSetupUI(bool isMultiplayer, GUIComponent newGameContainer, GUIComponent loadGameContainer, IEnumerable submarines, IEnumerable saveFiles = null) + private void UpdateNewGameMenu(IEnumerable submarines) { - this.isMultiplayer = isMultiplayer; - this.newGameContainer = newGameContainer; - this.loadGameContainer = loadGameContainer; + pageContainer = + new GUIListBox(new RectTransform(Vector2.One, newGameContainer.RectTransform), style: null, isHorizontal: true) + { + ScrollBarEnabled = false, + ScrollBarVisible = false, + HoverCursor = CursorState.Default + }; - var columnContainer = new GUILayoutGroup(new RectTransform(Vector2.One, newGameContainer.RectTransform), isHorizontal: true) + GUILayoutGroup createPageLayout() + { + var containerItem = + new GUIFrame(new RectTransform(Vector2.One, pageContainer.Content.RectTransform), style: null); + return new GUILayoutGroup(new RectTransform(Vector2.One * 0.95f, containerItem.RectTransform, + Anchor.Center)); + } + + CreateFirstPage(createPageLayout(), submarines); + CreateSecondPage(createPageLayout()); + + pageContainer.RecalculateChildren(); + pageContainer.GetAllChildren().ForEach(c => + { + c.ClampMouseRectToParent = true; + }); + pageContainer.GetAllChildren().ForEach(dd => + { + dd.ListBox.ClampMouseRectToParent = false; + dd.ListBox.Content.ClampMouseRectToParent = false; + }); + SetPage(0); + } + + private void CreateFirstPage(GUILayoutGroup firstPageLayout, IEnumerable submarines) + { + firstPageLayout.RelativeSpacing = 0.02f; + + var columnContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), firstPageLayout.RectTransform), isHorizontal: true) { Stretch = true, - RelativeSpacing = isMultiplayer ? 0.0f : 0.02f + RelativeSpacing = 0.02f }; var leftColumn = new GUILayoutGroup(new RectTransform(Vector2.One, columnContainer.RectTransform)) @@ -70,7 +120,7 @@ namespace Barotrauma RelativeSpacing = 0.015f }; - var rightColumn = new GUILayoutGroup(new RectTransform(isMultiplayer ? Vector2.Zero : new Vector2(1.5f, 1.0f), columnContainer.RectTransform)) + var rightColumn = new GUILayoutGroup(new RectTransform(new Vector2(1.5f, 1.0f), columnContainer.RectTransform)) { Stretch = true, RelativeSpacing = 0.015f @@ -88,47 +138,37 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.02f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("MapSeed"), font: GUI.SubHeadingFont); seedBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.05f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, ToolBox.RandomSeed(8)); - if (!isMultiplayer) + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.02f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("SelectedSub"), font: GUI.SubHeadingFont); + + var moddedDropdown = new GUIDropDown(new RectTransform(new Vector2(1f, 0.02f), leftColumn.RectTransform), "", 3); + moddedDropdown.AddItem(TextManager.Get("clientpermission.all"), CategoryFilter.All); + moddedDropdown.AddItem(TextManager.Get("servertag.modded.false"), CategoryFilter.Vanilla); + moddedDropdown.AddItem(TextManager.Get("customrank"), CategoryFilter.Custom); + moddedDropdown.Select(0); + + var filterContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), leftColumn.RectTransform), isHorizontal: true) { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.02f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("SelectedSub"), font: GUI.SubHeadingFont); + Stretch = true + }; - var moddedDropdown = new GUIDropDown(new RectTransform(new Vector2(1f, 0.02f), leftColumn.RectTransform), "", 3); - moddedDropdown.AddItem(TextManager.Get("clientpermission.all"), CategoryFilter.All); - moddedDropdown.AddItem(TextManager.Get("servertag.modded.false"), CategoryFilter.Vanilla); - moddedDropdown.AddItem(TextManager.Get("customrank"), CategoryFilter.Custom); - moddedDropdown.Select(0); + subList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.65f), leftColumn.RectTransform)) { ScrollBarVisible = true }; - var filterContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), leftColumn.RectTransform), isHorizontal: true) - { - Stretch = true - }; + var searchTitle = new GUITextBlock(new RectTransform(new Vector2(0.001f, 1.0f), filterContainer.RectTransform), TextManager.Get("serverlog.filter"), textAlignment: Alignment.CenterLeft, font: GUI.Font); + var searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 1.0f), filterContainer.RectTransform, Anchor.CenterRight), font: GUI.Font, createClearButton: true); + filterContainer.RectTransform.MinSize = searchBox.RectTransform.MinSize; + searchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; }; + searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; + searchBox.OnTextChanged += (textBox, text) => { FilterSubs(subList, text); return true; }; - subList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.65f), leftColumn.RectTransform)) { ScrollBarVisible = true }; - - var searchTitle = new GUITextBlock(new RectTransform(new Vector2(0.001f, 1.0f), filterContainer.RectTransform), TextManager.Get("serverlog.filter"), textAlignment: Alignment.CenterLeft, font: GUI.Font); - var searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 1.0f), filterContainer.RectTransform, Anchor.CenterRight), font: GUI.Font, createClearButton: true); - filterContainer.RectTransform.MinSize = searchBox.RectTransform.MinSize; - searchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; }; - searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; - searchBox.OnTextChanged += (textBox, text) => { FilterSubs(subList, text); return true; }; - - moddedDropdown.OnSelected = (component, data) => - { - searchBox.Text = string.Empty; - subFilter = (CategoryFilter)data; - UpdateSubList(SubmarineInfo.SavedSubmarines); - return true; - }; - - subList.OnSelected = OnSubSelected; - } - else // Spacing to fix the multiplayer campaign setup layout + moddedDropdown.OnSelected = (component, data) => { - CreateMultiplayerCampaignSubList(leftColumn.RectTransform); + searchBox.Text = string.Empty; + subFilter = (CategoryFilter)data; + UpdateSubList(SubmarineInfo.SavedSubmarines); + return true; + }; - //spacing - //new GUIFrame(new RectTransform(new Vector2(1.0f, 0.25f), leftColumn.RectTransform), style: null); - } + subList.OnSelected = OnSubSelected; // New game right side subPreviewContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), rightColumn.RectTransform)) @@ -136,176 +176,162 @@ namespace Barotrauma Stretch = true }; - var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.12f), - (isMultiplayer ? leftColumn : rightColumn).RectTransform) { MaxSize = new Point(int.MaxValue, 60) }, childAnchor: Anchor.BottomRight, isHorizontal: true); - if (!isMultiplayer) { buttonContainer.IgnoreLayoutGroups = true; } - - StartButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1f), buttonContainer.RectTransform, Anchor.BottomRight) { MaxSize = new Point(350, 60) }, TextManager.Get("StartCampaignButton")) + var firstPageButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.08f), + firstPageLayout.RectTransform), childAnchor: Anchor.BottomLeft, isHorizontal: true) { - OnClicked = (GUIButton btn, object userData) => - { - if (string.IsNullOrWhiteSpace(saveNameBox.Text)) - { - saveNameBox.Flash(GUI.Style.Red); - return false; - } - - SubmarineInfo selectedSub = null; - - if (!isMultiplayer) - { - if (!(subList.SelectedData is SubmarineInfo)) { return false; } - selectedSub = subList.SelectedData as SubmarineInfo; - } - else - { - if (GameMain.NetLobbyScreen.SelectedSub == null) { return false; } - selectedSub = GameMain.NetLobbyScreen.SelectedSub; - } - - if (selectedSub.SubmarineClass == SubmarineClass.Undefined) - { - new GUIMessageBox(TextManager.Get("error"), TextManager.Get("undefinedsubmarineselected")); - return false; - } - - if (string.IsNullOrEmpty(selectedSub.MD5Hash.Hash)) - { - ((GUITextBlock)subList.SelectedComponent).TextColor = Color.DarkRed * 0.8f; - subList.SelectedComponent.CanBeFocused = false; - subList.Deselect(); - return false; - } - - string savePath = SaveUtil.CreateSavePath(isMultiplayer ? SaveUtil.SaveType.Multiplayer : SaveUtil.SaveType.Singleplayer, saveNameBox.Text); - bool hasRequiredContentPackages = selectedSub.RequiredContentPackagesInstalled; - - CampaignSettings settings = new CampaignSettings(); - if (isMultiplayer) - { - settings.RadiationEnabled = GameMain.NetLobbyScreen.IsRadiationEnabled(); - settings.MaxMissionCount = GameMain.NetLobbyScreen.GetMaxMissionCount(); - } - else - { - settings.RadiationEnabled = EnableRadiationToggle?.Selected ?? false; - if (MaxMissionCountText != null && Int32.TryParse(MaxMissionCountText.Text, out int missionCount)) - { - settings.MaxMissionCount = missionCount; - } - else - { - settings.MaxMissionCount = CampaignSettings.DefaultMaxMissionCount; - } - } - - if (selectedSub.HasTag(SubmarineTag.Shuttle) || !hasRequiredContentPackages) - { - if (!hasRequiredContentPackages) - { - var msgBox = new GUIMessageBox(TextManager.Get("ContentPackageMismatch"), - TextManager.GetWithVariable("ContentPackageMismatchWarning", "[requiredcontentpackages]", string.Join(", ", selectedSub.RequiredContentPackages)), - new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); - - msgBox.Buttons[0].OnClicked = msgBox.Close; - msgBox.Buttons[0].OnClicked += (button, obj) => - { - if (GUIMessageBox.MessageBoxes.Count == 0) - { - StartNewGame?.Invoke(selectedSub, savePath, seedBox.Text, settings); - if (isMultiplayer) - { - CoroutineManager.StartCoroutine(WaitForCampaignSetup(), "WaitForCampaignSetup"); - } - } - return true; - }; - - msgBox.Buttons[1].OnClicked = msgBox.Close; - } - - if (selectedSub.HasTag(SubmarineTag.Shuttle)) - { - var msgBox = new GUIMessageBox(TextManager.Get("ShuttleSelected"), - TextManager.Get("ShuttleWarning"), - new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); - - msgBox.Buttons[0].OnClicked = (button, obj) => - { - StartNewGame?.Invoke(selectedSub, savePath, seedBox.Text, settings); - if (isMultiplayer) - { - CoroutineManager.StartCoroutine(WaitForCampaignSetup(), "WaitForCampaignSetup"); - } - return true; - }; - msgBox.Buttons[0].OnClicked += msgBox.Close; - - msgBox.Buttons[1].OnClicked = msgBox.Close; - return false; - } - } - else - { - StartNewGame?.Invoke(selectedSub, savePath, seedBox.Text, settings); - if (isMultiplayer) - { - CoroutineManager.StartCoroutine(WaitForCampaignSetup(), "WaitForCampaignSetup"); - } - } - - return true; - } + RelativeSpacing = 0.025f }; - InitialMoneyText = new GUITextBlock(new RectTransform(new Vector2(isMultiplayer ? 0.6f : 0.3f, 1f), buttonContainer.RectTransform), "", font: isMultiplayer ? GUI.Style.SmallFont : GUI.Style.Font, textColor: GUI.Style.Green) + InitialMoneyText = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1f), firstPageButtonContainer.RectTransform), "", font: GUI.Style.Font, textColor: GUI.Style.Green, textAlignment: Alignment.CenterLeft) { TextGetter = () => { int initialMoney = CampaignMode.InitialMoney; - if (isMultiplayer) - { - if (GameMain.NetLobbyScreen.SelectedSub != null) - { - initialMoney -= GameMain.NetLobbyScreen.SelectedSub.Price; - } - } - else if (subList.SelectedData is SubmarineInfo subInfo) + if (subList.SelectedData is SubmarineInfo subInfo) { initialMoney -= subInfo.Price; } - initialMoney = Math.Max(initialMoney, isMultiplayer ? MultiPlayerCampaign.MinimumInitialMoney : 0); + initialMoney = Math.Max(initialMoney, 0); return TextManager.GetWithVariable("campaignstartingmoney", "[money]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", initialMoney)); } }; - if (!isMultiplayer) + CampaignCustomizeButton = new GUIButton(new RectTransform(new Vector2(0.25f, 1f), firstPageButtonContainer.RectTransform, Anchor.CenterLeft), TextManager.Get("SettingsButton")) { - CampaignCustomizeButton = new GUIButton(new RectTransform(new Vector2(0.25f, 1f), buttonContainer.RectTransform, Anchor.CenterLeft), TextManager.Get("SettingsButton")) + OnClicked = (tb, userdata) => { - OnClicked = (tb, userdata) => - { - CreateCustomizeWindow(); - return true; - } - }; + CreateCustomizeWindow(); + return true; + } + }; - var disclaimerBtn = new GUIButton(new RectTransform(new Vector2(1.0f, 0.8f), rightColumn.RectTransform, Anchor.TopRight) { AbsoluteOffset = new Point(5) }, style: "GUINotificationButton") + nextButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1f), firstPageButtonContainer.RectTransform, Anchor.BottomRight), TextManager.Get("Next")) + { + OnClicked = (GUIButton btn, object userData) => { - IgnoreLayoutGroups = true, - OnClicked = (btn, userdata) => { GameMain.Instance.ShowCampaignDisclaimer(); return true; } - }; - disclaimerBtn.RectTransform.MaxSize = new Point((int)(30 * GUI.Scale)); - } + SetPage(1); + return false; + } + }; + + var disclaimerBtn = new GUIButton(new RectTransform(new Vector2(1.0f, 0.8f), rightColumn.RectTransform, Anchor.TopRight) { AbsoluteOffset = new Point(5) }, style: "GUINotificationButton") + { + IgnoreLayoutGroups = true, + OnClicked = (btn, userdata) => { GameMain.Instance.ShowCampaignDisclaimer(); return true; } + }; + disclaimerBtn.RectTransform.MaxSize = new Point((int)(30 * GUI.Scale)); columnContainer.Recalculate(); leftColumn.Recalculate(); rightColumn.Recalculate(); if (submarines != null) { UpdateSubList(submarines); } - UpdateLoadMenu(saveFiles); + } + + private void CreateSecondPage(GUILayoutGroup secondPageLayout) + { + secondPageLayout.RelativeSpacing = 0.01f; + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.04f), secondPageLayout.RectTransform), + TextManager.Get("Crew"), font: GUI.Style.SubHeadingFont, textAlignment: Alignment.TopLeft); + + characterInfoColumns = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.86f), secondPageLayout.RectTransform), isHorizontal: true) + { + Stretch = true, + RelativeSpacing = 0.01f + }; + + var secondPageButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.08f), + secondPageLayout.RectTransform), childAnchor: Anchor.BottomLeft, isHorizontal: true) + { + RelativeSpacing = 0.2f + }; + + var backButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1f), secondPageButtonContainer.RectTransform, Anchor.BottomRight), TextManager.Get("Back")) + { + OnClicked = (GUIButton btn, object userData) => + { + SetPage(0); + return false; + } + }; + + StartButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1f), secondPageButtonContainer.RectTransform, Anchor.BottomRight), TextManager.Get("StartCampaignButton")) + { + OnClicked = FinishSetup + }; } + public void RandomizeCrew() + { + var characterInfos = new List<(CharacterInfo Info, JobPrefab Job)>(); + foreach (JobPrefab jobPrefab in JobPrefab.Prefabs) + { + for (int i = 0; i < jobPrefab.InitialCount; i++) + { + var variant = Rand.Range(0, jobPrefab.Variants); + characterInfos.Add((new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: jobPrefab, variant: variant), jobPrefab)); + } + } + + characterInfoColumns.ClearChildren(); + CharacterMenus?.ForEach(m => m.Dispose()); + CharacterMenus = new CharacterInfo.AppearanceCustomizationMenu[characterInfos.Count]; + + for (int i = 0; i < characterInfos.Count; i++) + { + var subLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f / characterInfos.Count, 1.0f), + characterInfoColumns.RectTransform)); + + var (characterInfo, job) = characterInfos[i]; + + 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 characterName = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.1f), subLayout.RectTransform)) + { + Text = characterInfo.Name, + UserData = "random" + }; + characterName.OnDeselected += (sender, key) => + { + if (string.IsNullOrWhiteSpace(sender.Text)) + { + characterInfo.Name = characterInfo.GetRandomName(Rand.RandSync.Unsynced); + sender.Text = characterInfo.Name; + sender.UserData = "random"; + } + else + { + characterInfo.Name = sender.Text; + sender.UserData = "user"; + } + }; + characterName.OnEnterPressed += (sender, text) => + { + sender.Deselect(); + return false; + }; + + var customizationFrame = + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.6f), subLayout.RectTransform), style: null); + CharacterMenus[i] = + new CharacterInfo.AppearanceCustomizationMenu(characterInfo, customizationFrame, hasIcon: false) + { + OnHeadSwitch = menu => + { + if (characterName.UserData is string ud && ud == "random") + { + characterInfo.Name = characterInfo.GetRandomName(Rand.RandSync.Unsynced); + characterName.Text = characterInfo.Name; + characterName.UserData = "random"; + } + } + }; + } + } + private void CreateCustomizeWindow() { CampaignCustomizeSettings = new GUIMessageBox("", "", new string[] { TextManager.Get("OK") }, new Vector2(0.2f, 0.2f)); @@ -355,106 +381,93 @@ namespace Barotrauma maxMissionCountContainer.Children.ForEach(c => c.ToolTip = maxMissionCountSettingHolder.ToolTip); } - private void CreateMultiplayerCampaignSubList(RectTransform parent) + private bool FinishSetup(GUIButton btn, object userdata) { - GUILayoutGroup subHolder = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.725f), parent)) + if (string.IsNullOrWhiteSpace(saveNameBox.Text)) { - RelativeSpacing = 0.005f, - Stretch = true - }; + saveNameBox.Flash(GUI.Style.Red); + return false; + } - var subLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), subHolder.RectTransform) { MinSize = new Point(0, 25) }, TextManager.Get("purchasablesubmarines", fallBackTag: "workshoplabelsubmarines"), font: GUI.SubHeadingFont); + SubmarineInfo selectedSub = null; - var filterContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), subHolder.RectTransform), isHorizontal: true) + if (!(subList.SelectedData is SubmarineInfo)) { return false; } + selectedSub = subList.SelectedData as SubmarineInfo; + + if (selectedSub.SubmarineClass == SubmarineClass.Undefined) { - Stretch = true - }; - var searchTitle = new GUITextBlock(new RectTransform(new Vector2(0.001f, 1.0f), filterContainer.RectTransform), TextManager.Get("serverlog.filter"), textAlignment: Alignment.CenterLeft, font: GUI.Font); - var searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 1.0f), filterContainer.RectTransform, Anchor.CenterRight), font: GUI.Font, createClearButton: true); - filterContainer.RectTransform.MinSize = searchBox.RectTransform.MinSize; - searchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; }; - searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; - searchBox.OnTextChanged += (textBox, text) => + new GUIMessageBox(TextManager.Get("error"), TextManager.Get("undefinedsubmarineselected")); + return false; + } + + if (string.IsNullOrEmpty(selectedSub.MD5Hash.Hash)) { - foreach (GUIComponent child in subList.Content.Children) + ((GUITextBlock)subList.SelectedComponent).TextColor = Color.DarkRed * 0.8f; + subList.SelectedComponent.CanBeFocused = false; + subList.Deselect(); + return false; + } + + string savePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Singleplayer, saveNameBox.Text); + bool hasRequiredContentPackages = selectedSub.RequiredContentPackagesInstalled; + + CampaignSettings settings = new CampaignSettings(); + settings.RadiationEnabled = EnableRadiationToggle?.Selected ?? false; + if (MaxMissionCountText != null && Int32.TryParse(MaxMissionCountText.Text, out int missionCount)) + { + settings.MaxMissionCount = missionCount; + } + else + { + settings.MaxMissionCount = CampaignSettings.DefaultMaxMissionCount; + } + + if (selectedSub.HasTag(SubmarineTag.Shuttle) || !hasRequiredContentPackages) + { + if (!hasRequiredContentPackages) { - if (!(child.UserData is SubmarineInfo sub)) { continue; } - child.Visible = string.IsNullOrEmpty(text) ? true : sub.DisplayName.ToLower().Contains(text.ToLower()); - } - return true; - }; + var msgBox = new GUIMessageBox(TextManager.Get("ContentPackageMismatch"), + TextManager.GetWithVariable("ContentPackageMismatchWarning", "[requiredcontentpackages]", string.Join(", ", selectedSub.RequiredContentPackages)), + new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); - subList = new GUIListBox(new RectTransform(Vector2.One, subHolder.RectTransform)); - subTickBoxes = new List(); - - for (int i = 0; i < GameMain.Client.ServerSubmarines.Count; i++) - { - SubmarineInfo sub = GameMain.Client.ServerSubmarines[i]; - - if (!sub.IsCampaignCompatible) continue; - - var frame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.2f), subList.Content.RectTransform) { MinSize = new Point(0, 20) }, - style: "ListBoxElement") - { - ToolTip = sub.Description, - UserData = sub - }; - - int buttonSize = (int)(frame.Rect.Height * 0.8f); - - GUITickBox tickBox = new GUITickBox(new RectTransform(new Vector2(0.8f, 1.0f), frame.RectTransform, Anchor.CenterLeft), ToolBox.LimitString(sub.DisplayName, GUI.Font, subList.Content.Rect.Width - 65)) - { - UserData = sub, - OnSelected = (GUITickBox box) => + msgBox.Buttons[0].OnClicked = msgBox.Close; + msgBox.Buttons[0].OnClicked += (button, obj) => { - GameMain.Client.RequestCampaignSub(box.UserData as SubmarineInfo, box.Selected); + if (GUIMessageBox.MessageBoxes.Count == 0) + { + StartNewGame?.Invoke(selectedSub, savePath, seedBox.Text, settings); + } return true; - } - }; - subTickBoxes.Add(tickBox); - tickBox.Selected = GameMain.NetLobbyScreen.CampaignSubmarines.Contains(sub); + }; - frame.RectTransform.MinSize = new Point(0, tickBox.RectTransform.MinSize.Y); - - var subTextBlock = tickBox.TextBlock; - - var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == sub.Name && s.MD5Hash?.Hash == sub.MD5Hash?.Hash); - if (matchingSub == null) matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == sub.Name); - - if (matchingSub == null) - { - subTextBlock.TextColor = new Color(subTextBlock.TextColor, 0.5f); - frame.ToolTip = TextManager.Get("SubNotFound"); - } - else if (matchingSub?.MD5Hash == null || matchingSub.MD5Hash?.Hash != sub.MD5Hash?.Hash) - { - subTextBlock.TextColor = new Color(subTextBlock.TextColor, 0.5f); - frame.ToolTip = TextManager.Get("SubDoesntMatch"); + msgBox.Buttons[1].OnClicked = msgBox.Close; } - if (!sub.RequiredContentPackagesInstalled) + if (selectedSub.HasTag(SubmarineTag.Shuttle)) { - subTextBlock.TextColor = Color.Lerp(subTextBlock.TextColor, Color.DarkRed, 0.5f); - frame.ToolTip = TextManager.Get("ContentPackageMismatch") + "\n\n" + frame.RawToolTip; - } + var msgBox = new GUIMessageBox(TextManager.Get("ShuttleSelected"), + TextManager.Get("ShuttleWarning"), + new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); - var classText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), frame.RectTransform, Anchor.CenterRight), - TextManager.Get($"submarineclass.{sub.SubmarineClass}"), textAlignment: Alignment.CenterRight, font: GUI.SmallFont) - { - TextColor = subTextBlock.TextColor * 0.8f, - ToolTip = subTextBlock.RawToolTip - }; + msgBox.Buttons[0].OnClicked = (button, obj) => + { + StartNewGame?.Invoke(selectedSub, savePath, seedBox.Text, settings); + return true; + }; + msgBox.Buttons[0].OnClicked += msgBox.Close; + + msgBox.Buttons[1].OnClicked = msgBox.Close; + return false; + } } - } - - public void RefreshMultiplayerCampaignSubUI(List campaignSubs) - { - for (int i = 0; i < subTickBoxes.Count; i++) + else { - subTickBoxes[i].Selected = campaignSubs.Contains(subTickBoxes[i].UserData as SubmarineInfo); + StartNewGame?.Invoke(selectedSub, savePath, seedBox.Text, settings); } - } + return true; + } + public void RandomizeSeed() { seedBox.Text = ToolBox.RandomSeed(8); @@ -478,54 +491,28 @@ namespace Barotrauma if (!(obj is SubmarineInfo sub)) { return true; } #if !DEBUG - if (!isMultiplayer && sub.Price > CampaignMode.InitialMoney && !GameMain.DebugDraw) + if (sub.Price > CampaignMode.InitialMoney && !GameMain.DebugDraw) { - StartButton.Enabled = false; + SetPage(0); + nextButton.Enabled = false; return false; } #endif - StartButton.Enabled = true; + nextButton.Enabled = true; sub.CreatePreviewWindow(subPreviewContainer); return true; } - private IEnumerable WaitForCampaignSetup() - { - GUI.SetCursorWaiting(); - string headerText = TextManager.Get("CampaignStartingPleaseWait"); - var msgBox = new GUIMessageBox(headerText, TextManager.Get("CampaignStarting"), new string[] { TextManager.Get("Cancel") }); - - msgBox.Buttons[0].OnClicked = (btn, userdata) => - { - GameMain.NetLobbyScreen.HighlightMode(GameMain.NetLobbyScreen.SelectedModeIndex); - GameMain.NetLobbyScreen.SelectMode(GameMain.NetLobbyScreen.SelectedModeIndex); - GUI.ClearCursorWait(); - CoroutineManager.StopCoroutines("WaitForCampaignSetup"); - return true; - }; - msgBox.Buttons[0].OnClicked += msgBox.Close; - - DateTime timeOut = DateTime.Now + new TimeSpan(0, 0, 20); - while (Screen.Selected != GameMain.GameScreen && DateTime.Now < timeOut) - { - msgBox.Header.Text = headerText + new string('.', (int)Timing.TotalTime % 3 + 1); - yield return CoroutineStatus.Running; - } - msgBox.Close(); - GUI.ClearCursorWait(); - yield return CoroutineStatus.Success; - } - public void CreateDefaultSaveName() { - string savePath = SaveUtil.CreateSavePath(isMultiplayer ? SaveUtil.SaveType.Multiplayer : SaveUtil.SaveType.Singleplayer); + string savePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Singleplayer); saveNameBox.Text = Path.GetFileNameWithoutExtension(savePath); } public void UpdateSubList(IEnumerable submarines) { List subsToShow; - if (!isMultiplayer && subFilter != CategoryFilter.All) + if (subFilter != CategoryFilter.All) { subsToShow = submarines.Where(s => s.IsCampaignCompatibleIgnoreClass && s.IsVanillaSubmarine() == (subFilter == CategoryFilter.Vanilla)).ToList(); } @@ -596,10 +583,10 @@ namespace Barotrauma if (saveFiles == null) { - saveFiles = SaveUtil.GetSaveFiles(isMultiplayer ? SaveUtil.SaveType.Multiplayer : SaveUtil.SaveType.Singleplayer); + saveFiles = SaveUtil.GetSaveFiles(SaveUtil.SaveType.Singleplayer); } - var leftColumn = new GUILayoutGroup(new RectTransform(isMultiplayer ? new Vector2(1.0f, 0.85f) : new Vector2(0.5f, 1.0f), loadGameContainer.RectTransform), childAnchor: Anchor.TopCenter) + var leftColumn = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), loadGameContainer.RectTransform), childAnchor: Anchor.TopCenter) { Stretch = true, RelativeSpacing = 0.03f @@ -610,26 +597,23 @@ namespace Barotrauma OnSelected = SelectSaveFile }; - if (!isMultiplayer) + new GUIButton(new RectTransform(new Vector2(0.6f, 0.08f), leftColumn.RectTransform), TextManager.Get("showinfolder")) { - new GUIButton(new RectTransform(new Vector2(0.6f, 0.08f), leftColumn.RectTransform), TextManager.Get("showinfolder")) + OnClicked = (btn, userdata) => { - OnClicked = (btn, userdata) => + try { - try - { - ToolBox.OpenFileWithShell(SaveUtil.SaveFolder); - } - catch (Exception e) - { - new GUIMessageBox( - TextManager.Get("error"), - TextManager.GetWithVariables("showinfoldererror", new string[] { "[folder]", "[errormessage]" }, new string[] { SaveUtil.SaveFolder, e.Message })); - } - return true; + ToolBox.OpenFileWithShell(SaveUtil.SaveFolder); } - }; - } + catch (Exception e) + { + new GUIMessageBox( + TextManager.Get("error"), + TextManager.GetWithVariables("showinfoldererror", new string[] { "[folder]", "[errormessage]" }, new string[] { SaveUtil.SaveFolder, e.Message })); + } + return true; + } + }; foreach (string saveFile in saveFiles) { @@ -649,39 +633,27 @@ namespace Barotrauma bool isCompatible = true; prevSaveFiles ??= new List(); - if (!isMultiplayer) - { - nameText.Text = Path.GetFileNameWithoutExtension(saveFile); - XDocument doc = SaveUtil.LoadGameSessionDoc(saveFile); - if (doc?.Root == null) - { - DebugConsole.ThrowError("Error loading save file \"" + saveFile + "\". The file may be corrupted."); - nameText.TextColor = GUI.Style.Red; - continue; - } - if (doc.Root.GetChildElement("multiplayercampaign") != null) - { - //multiplayer campaign save in the wrong folder -> don't show the save - saveList.Content.RemoveChild(saveFrame); - continue; - } - subName = doc.Root.GetAttributeString("submarine", ""); - saveTime = doc.Root.GetAttributeString("savetime", ""); - isCompatible = SaveUtil.IsSaveFileCompatible(doc); - contentPackageStr = doc.Root.GetAttributeString("selectedcontentpackages", ""); - prevSaveFiles?.Add(saveFile); - } - else + nameText.Text = Path.GetFileNameWithoutExtension(saveFile); + XDocument doc = SaveUtil.LoadGameSessionDoc(saveFile); + + if (doc?.Root == null) { - prevSaveFiles?.Add(saveFile); - string[] splitSaveFile = saveFile.Split(';'); - saveFrame.UserData = splitSaveFile[0]; - fileName = nameText.Text = Path.GetFileNameWithoutExtension(splitSaveFile[0]); - if (splitSaveFile.Length > 1) { subName = splitSaveFile[1]; } - if (splitSaveFile.Length > 2) { saveTime = splitSaveFile[2]; } - if (splitSaveFile.Length > 3) { contentPackageStr = splitSaveFile[3]; } + DebugConsole.ThrowError("Error loading save file \"" + saveFile + "\". The file may be corrupted."); + nameText.TextColor = GUI.Style.Red; + continue; } + if (doc.Root.GetChildElement("multiplayercampaign") != null) + { + //multiplayer campaign save in the wrong folder -> don't show the save + saveList.Content.RemoveChild(saveFrame); + continue; + } + subName = doc.Root.GetAttributeString("submarine", ""); + saveTime = doc.Root.GetAttributeString("savetime", ""); + isCompatible = SaveUtil.IsSaveFileCompatible(doc); + contentPackageStr = doc.Root.GetAttributeString("selectedcontentpackages", ""); + prevSaveFiles?.Add(saveFile); if (!string.IsNullOrEmpty(saveTime) && long.TryParse(saveTime, out long unixTime)) { DateTime time = ToolBox.Epoch.ToDateTime(unixTime); @@ -748,38 +720,16 @@ namespace Barotrauma { if (string.IsNullOrWhiteSpace(saveList.SelectedData as string)) { return false; } LoadGame?.Invoke(saveList.SelectedData as string); - if (isMultiplayer) - { - CoroutineManager.StartCoroutine(WaitForCampaignSetup(), "WaitForCampaignSetup"); - } return true; }, Enabled = false }; - deleteMpSaveButton = new GUIButton(new RectTransform(new Vector2(0.45f, 0.12f), loadGameContainer.RectTransform, Anchor.BottomLeft), - TextManager.Get("Delete"), style: "GUIButtonSmall") - { - OnClicked = DeleteSave, - Visible = false - }; } private bool SelectSaveFile(GUIComponent component, object obj) { string fileName = (string)obj; - if (isMultiplayer) - { - loadGameButton.Enabled = true; - deleteMpSaveButton.Visible = deleteMpSaveButton.Enabled = GameMain.Client.IsServerOwner; - deleteMpSaveButton.Enabled = GameMain.GameSession?.SavePath != fileName; - if (deleteMpSaveButton.Visible) - { - deleteMpSaveButton.UserData = obj as string; - } - return true; - } - XDocument doc = SaveUtil.LoadGameSessionDoc(fileName); if (doc?.Root == null) { @@ -868,6 +818,5 @@ namespace Barotrauma } loadGameContainer.RemoveChild(prevFrame); } - } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index 9f158fe81..38e6145a0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -182,7 +182,7 @@ namespace Barotrauma.CharacterEditor showSpritesheet = false; isFrozen = false; autoFreeze = false; - limbPairEditing = true; + limbPairEditing = false; uniformScaling = true; lockSpriteOrigin = true; lockSpritePosition = false; @@ -1581,7 +1581,6 @@ namespace Barotrauma.CharacterEditor Character.Controlled = character; SetWallCollisions(character.AnimController.forceStanding); CreateTextures(); - limbPairEditing = character.IsHumanoid; CreateGUI(); ClearWidgets(); ClearSelection(); @@ -1803,6 +1802,7 @@ namespace Barotrauma.CharacterEditor { case AnimationType.Walk: case AnimationType.Run: + case AnimationType.Crouch: if (!ragdollParams.CanWalk) { continue; } break; case AnimationType.SwimSlow: @@ -1819,8 +1819,8 @@ namespace Barotrauma.CharacterEditor { AllFiles.Add(configFilePath); } - limbPairEditing = false; SpawnCharacter(configFilePath, ragdollParams); + limbPairEditing = false; limbsToggle.Selected = true; recalculateColliderToggle.Selected = true; lockSpriteOriginToggle.Selected = false; @@ -2599,11 +2599,15 @@ namespace Barotrauma.CharacterEditor var layoutGroupAnimation = new GUILayoutGroup(new RectTransform(Vector2.One, animationControls.RectTransform), childAnchor: Anchor.TopLeft) { CanBeFocused = false }; var animationSelectionElement = new GUIFrame(new RectTransform(new Point(elementSize.X * 2 - (int)(5 * GUI.xScale), elementSize.Y), layoutGroupAnimation.RectTransform), style: null); var animationSelectionText = new GUITextBlock(new RectTransform(new Point(elementSize.X, elementSize.Y), animationSelectionElement.RectTransform), GetCharacterEditorTranslation("SelectedAnimation") + ": ", Color.WhiteSmoke, textAlignment: Alignment.Center); - animSelection = new GUIDropDown(new RectTransform(new Point((int)(100 * GUI.xScale), elementSize.Y), animationSelectionElement.RectTransform, Anchor.TopRight), elementCount: 4); + animSelection = new GUIDropDown(new RectTransform(new Point((int)(100 * GUI.xScale), elementSize.Y), animationSelectionElement.RectTransform, Anchor.TopRight), elementCount: 5); if (character.AnimController.CanWalk) { animSelection.AddItem(AnimationType.Walk.ToString(), AnimationType.Walk); animSelection.AddItem(AnimationType.Run.ToString(), AnimationType.Run); + if (character.IsHumanoid) + { + animSelection.AddItem(AnimationType.Crouch.ToString(), AnimationType.Crouch); + } } animSelection.AddItem(AnimationType.SwimSlow.ToString(), AnimationType.SwimSlow); animSelection.AddItem(AnimationType.SwimFast.ToString(), AnimationType.SwimFast); @@ -2622,25 +2626,15 @@ namespace Barotrauma.CharacterEditor switch (character.AnimController.ForceSelectAnimationType) { case AnimationType.Walk: - character.AnimController.forceStanding = true; - character.ForceRun = false; - if (!wallCollisionsEnabled) - { - SetWallCollisions(true); - } - if (previousAnim != AnimationType.Walk && previousAnim != AnimationType.Run) - { - TeleportTo(spawnPosition); - } - break; case AnimationType.Run: + case AnimationType.Crouch: character.AnimController.forceStanding = true; - character.ForceRun = true; + character.ForceRun = character.AnimController.ForceSelectAnimationType == AnimationType.Run; if (!wallCollisionsEnabled) { SetWallCollisions(true); } - if (previousAnim != AnimationType.Walk && previousAnim != AnimationType.Run) + if (previousAnim != AnimationType.Walk && previousAnim != AnimationType.Run && previousAnim != AnimationType.Crouch) { TeleportTo(spawnPosition); } @@ -3046,21 +3040,24 @@ namespace Barotrauma.CharacterEditor loadBox.Buttons[1].OnClicked += (btn, data) => { string fileName = Path.GetFileNameWithoutExtension(selectedFile); - if (character.IsHumanoid) + if (character.IsHumanoid && character.AnimController is HumanoidAnimController humanAnimController) { switch (selectedType) { case AnimationType.Walk: - character.AnimController.WalkParams = HumanWalkParams.GetAnimParams(character, fileName); + humanAnimController.WalkParams = HumanWalkParams.GetAnimParams(character, fileName); break; case AnimationType.Run: - character.AnimController.RunParams = HumanRunParams.GetAnimParams(character, fileName); + humanAnimController.RunParams = HumanRunParams.GetAnimParams(character, fileName); + break; + case AnimationType.Crouch: + humanAnimController.HumanCrouchParams = HumanCrouchParams.GetAnimParams(character, fileName); break; case AnimationType.SwimSlow: - character.AnimController.SwimSlowParams = HumanSwimSlowParams.GetAnimParams(character, fileName); + humanAnimController.SwimSlowParams = HumanSwimSlowParams.GetAnimParams(character, fileName); break; case AnimationType.SwimFast: - character.AnimController.SwimFastParams = HumanSwimFastParams.GetAnimParams(character, fileName); + humanAnimController.SwimFastParams = HumanSwimFastParams.GetAnimParams(character, fileName); break; default: DebugConsole.ThrowError(GetCharacterEditorTranslation("AnimationTypeNotImplemented").Replace("[type]", selectedType.ToString())); @@ -4152,7 +4149,7 @@ namespace Barotrauma.CharacterEditor float offset = 0.1f; w.refresh = () => { - var refPoint = SimToScreen(collider.SimPosition + GetSimSpaceForward() * offset); + var refPoint = SimToScreen(character.AnimController.Collider.SimPosition + GetSimSpaceForward() * offset); var handMovement = ConvertUnits.ToDisplayUnits(humanGroundedParams.HandMoveAmount); w.DrawPos = refPoint + new Vector2(handMovement.X * character.AnimController.Dir, handMovement.Y) * Cam.Zoom; }; @@ -4167,7 +4164,7 @@ namespace Barotrauma.CharacterEditor { if (w.IsSelected) { - GUI.DrawLine(sp, w.DrawPos, SimToScreen(collider.SimPosition + GetSimSpaceForward() * offset), GUI.Style.Green); + GUI.DrawLine(sp, w.DrawPos, SimToScreen(character.AnimController.Collider.SimPosition + GetSimSpaceForward() * offset), GUI.Style.Green); } }; }).Draw(spriteBatch, deltaTime); @@ -4731,7 +4728,7 @@ namespace Barotrauma.CharacterEditor rotation: 0, origin: orig, sourceRectangle: wearable.InheritSourceRect ? limb.ActiveSprite.SourceRect : wearable.Sprite.SourceRect, - scale: (wearable.InheritTextureScale ? 1 : wearable.Scale / RagdollParams.TextureScale) * spriteSheetZoom, + scale: (wearable.InheritScale ? 1 : wearable.Scale / RagdollParams.TextureScale) * spriteSheetZoom, effects: SpriteEffects.None, color: Color.White, layerDepth: 0); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs index e5a343cbe..5c41420ac 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs @@ -284,7 +284,7 @@ namespace Barotrauma.CharacterEditor } isTextureSelected = true; - texturePathElement.Text = destinationPath; + texturePathElement.Text = destinationPath.CleanUpPath(); }; FileSelection.ClearFileTypeFilters(); FileSelection.AddFileTypeFilter("PNG", "*.png"); @@ -431,7 +431,7 @@ namespace Barotrauma.CharacterEditor box.Header.Font = GUI.LargeFont; box.Content.ChildAnchor = Anchor.TopCenter; box.Content.AbsoluteSpacing = (int)(20 * GUI.Scale); - int elementSize = (int)(30 * GUI.Scale); + int elementSize = (int)(40 * GUI.Scale); var frame = new GUIFrame(new RectTransform(new Point(box.Content.Rect.Width - (int)(80 * GUI.xScale), box.Content.Rect.Height - (int)(200 * GUI.yScale)), box.Content.RectTransform, Anchor.Center), style: null, color: ParamsEditor.Color) { @@ -625,8 +625,12 @@ namespace Barotrauma.CharacterEditor var jointsElement = new GUIFrame(new RectTransform(new Vector2(1, 0.05f), content.RectTransform), style: null) { CanBeFocused = false }; new GUITextBlock(new RectTransform(new Vector2(0.2f, 1f), jointsElement.RectTransform), GetCharacterEditorTranslation("Joints"), font: GUI.SubHeadingFont); var jointButtonElement = new GUIFrame(new RectTransform(new Vector2(0.5f, 1f), jointsElement.RectTransform) - { RelativeOffset = new Vector2(0.15f, 0) }, style: null) - { CanBeFocused = false }; + { + RelativeOffset = new Vector2(0.15f, 0) + }, style: null) + { + CanBeFocused = false + }; var jointsList = new GUIListBox(new RectTransform(new Vector2(1, 0.45f), content.RectTransform)); var removeJointButton = new GUIButton(new RectTransform(new Point(jointButtonElement.Rect.Height, jointButtonElement.Rect.Height), jointButtonElement.RectTransform), style: "GUIMinusButton") { @@ -824,7 +828,7 @@ namespace Barotrauma.CharacterEditor { CanBeFocused = false }; - var group = new GUILayoutGroup(new RectTransform(Vector2.One, limbElement.RectTransform)) { AbsoluteSpacing = 2 }; + var group = new GUILayoutGroup(new RectTransform(Vector2.One, limbElement.RectTransform)) { AbsoluteSpacing = 16 }; var label = new GUITextBlock(new RectTransform(new Point(group.Rect.Width, elementSize), group.RectTransform), name, font: GUI.SubHeadingFont); var idField = new GUIFrame(new RectTransform(new Point(group.Rect.Width, elementSize), group.RectTransform), style: null); var nameField = new GUIFrame(new RectTransform(new Point(group.Rect.Width, elementSize), group.RectTransform), style: null); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs index a2420730e..da8e8bf52 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs @@ -25,6 +25,7 @@ namespace Barotrauma public Effect PostProcessEffect { get; private set; } public Effect GradientEffect { get; private set; } public Effect GrainEffect { get; private set; } + public Effect ThresholdTintEffect { get; private set; } public Effect BlueprintEffect { get; set; } public GameScreen(GraphicsDevice graphics, ContentManager content) @@ -38,21 +39,20 @@ namespace Barotrauma CreateRenderTargets(graphics); }; + Effect LoadEffect(string path) + => content.Load(path #if LINUX || OSX - //var blurEffect = content.Load("Effects/blurshader_opengl"); - damageEffect = content.Load("Effects/damageshader_opengl"); - PostProcessEffect = content.Load("Effects/postprocess_opengl"); - GradientEffect = content.Load("Effects/gradientshader_opengl"); - GrainEffect = content.Load("Effects/grainshader_opengl"); - BlueprintEffect = content.Load("Effects/blueprintshader_opengl"); -#else - //var blurEffect = content.Load("Effects/blurshader"); - damageEffect = content.Load("Effects/damageshader"); - PostProcessEffect = content.Load("Effects/postprocess"); - GradientEffect = content.Load("Effects/gradientshader"); - GrainEffect = content.Load("Effects/grainshader"); - BlueprintEffect = content.Load("Effects/blueprintshader"); + +"_opengl" #endif + ); + + //var blurEffect = LoadEffect("Effects/blurshader"); + damageEffect = LoadEffect("Effects/damageshader"); + PostProcessEffect = LoadEffect("Effects/postprocess"); + GradientEffect = LoadEffect("Effects/gradientshader"); + GrainEffect = LoadEffect("Effects/grainshader"); + ThresholdTintEffect = LoadEffect("Effects/thresholdtint"); + BlueprintEffect = LoadEffect("Effects/blueprintshader"); damageStencil = TextureLoader.FromFile("Content/Map/walldamage.png"); damageEffect.Parameters["xStencil"].SetValue(damageStencil); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index dcb4ae206..6c108709b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -40,7 +40,7 @@ namespace Barotrauma private readonly GUIFrame[] menuTabs; - private CampaignSetupUI campaignSetupUI; + private SinglePlayerCampaignSetupUI campaignSetupUI; private GUITextBox serverNameBox, /*portBox, queryPortBox,*/ passwordBox, maxPlayersBox; private GUITickBox isPublicBox, wrongPasswordBanBox, karmaBox; @@ -594,6 +594,8 @@ namespace Barotrauma GameMain.Instance.ShowCampaignDisclaimer(() => { SelectTab(null, Tab.NewGame); }); return true; } + campaignSetupUI.RandomizeCrew(); + campaignSetupUI.SetPage(0); campaignSetupUI.CreateDefaultSaveName(); campaignSetupUI.RandomizeSeed(); campaignSetupUI.UpdateSubList(SubmarineInfo.SavedSubmarines); @@ -985,24 +987,32 @@ namespace Barotrauma if (selectedTab < Tab.Empty && menuTabs[(int)selectedTab] != null) { menuTabs[(int)selectedTab].AddToGUIUpdateList(); + switch (selectedTab) + { + case Tab.NewGame: + campaignSetupUI.CharacterMenus?.ForEach(m => m.AddToGUIUpdateList()); + break; + } } } public override void Update(double deltaTime) { -#if !DEBUG -#if USE_STEAM +#if !DEBUG && USE_STEAM if (GameMain.Config.UseSteamMatchmaking) { hostServerButton.Enabled = Steam.SteamManager.IsInitialized; } steamWorkshopButton.Enabled = Steam.SteamManager.IsInitialized; -#endif -#else -#if USE_STEAM +#elif USE_STEAM steamWorkshopButton.Enabled = true; #endif -#endif + switch (selectedTab) + { + case Tab.NewGame: + campaignSetupUI.Update(); + break; + } } public void DrawBackground(GraphicsDevice graphics, SpriteBatch spriteBatch) @@ -1119,6 +1129,11 @@ namespace Barotrauma selectedSub = new SubmarineInfo(Path.Combine(SaveUtil.TempPath, selectedSub.Name + ".sub")); GameMain.GameSession = new GameSession(selectedSub, saveName, GameModePreset.SinglePlayerCampaign, settings, mapSeed); + GameMain.GameSession.CrewManager.CharacterInfos.Clear(); + foreach (var characterInfo in campaignSetupUI.CharacterMenus.Select(m => m.CharacterInfo)) + { + GameMain.GameSession.CrewManager.AddCharacterInfo(characterInfo); + } ((SinglePlayerCampaign)GameMain.GameSession.GameMode).LoadNewLevel(); } @@ -1146,7 +1161,7 @@ namespace Barotrauma menuTabs[(int)Tab.NewGame].ClearChildren(); menuTabs[(int)Tab.LoadGame].ClearChildren(); - var innerNewGame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), menuTabs[(int)Tab.NewGame].RectTransform, Anchor.Center) { RelativeOffset = new Vector2(0.0f, 0.025f) }) + var innerNewGame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), menuTabs[(int)Tab.NewGame].RectTransform, Anchor.Center)) { Stretch = true, RelativeSpacing = 0.02f @@ -1154,30 +1169,14 @@ namespace Barotrauma var newGameContent = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.95f), innerNewGame.RectTransform, Anchor.Center), style: "InnerFrame"); - var paddedNewGame = new GUIFrame(new RectTransform(new Vector2(0.95f), newGameContent.RectTransform, Anchor.Center), style: null); var paddedLoadGame = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.9f), menuTabs[(int)Tab.LoadGame].RectTransform, Anchor.Center) { AbsoluteOffset = new Point(0, 10) }, style: null); - campaignSetupUI = new CampaignSetupUI(false, paddedNewGame, paddedLoadGame, SubmarineInfo.SavedSubmarines) + campaignSetupUI = new SinglePlayerCampaignSetupUI(newGameContent, paddedLoadGame, SubmarineInfo.SavedSubmarines) { LoadGame = LoadGame, StartNewGame = StartGame }; - - var startButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), innerNewGame.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.BottomRight) - { - RelativeSpacing = 0.05f - }; - campaignSetupUI.StartButton.RectTransform.Parent = startButtonContainer.RectTransform; - campaignSetupUI.StartButton.RectTransform.MinSize = new Point( - (int)(campaignSetupUI.StartButton.TextBlock.TextSize.X * 1.5f), - campaignSetupUI.StartButton.RectTransform.MinSize.Y); - startButtonContainer.RectTransform.MinSize = new Point(0, campaignSetupUI.StartButton.RectTransform.MinSize.Y); - if (campaignSetupUI.CampaignCustomizeButton != null) - { - campaignSetupUI.CampaignCustomizeButton.RectTransform.Parent = startButtonContainer.RectTransform; - } - campaignSetupUI.InitialMoneyText.RectTransform.Parent = startButtonContainer.RectTransform; } private void CreateHostServerFields() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index 7944e07d2..62d6c6aad 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -12,7 +12,6 @@ namespace Barotrauma { partial class NetLobbyScreen : Screen { - private readonly List characterSprites = new List(); //private readonly List jobPreferenceSprites = new List(); private readonly GUIFrame infoFrame, modeFrame; @@ -65,7 +64,7 @@ namespace Barotrauma private readonly GUITickBox[] missionTypeTickBoxes; private readonly GUIListBox missionTypeList; - + public GUITextBox SeedBox { get; private set; @@ -78,11 +77,12 @@ namespace Barotrauma public static GUIButton JobInfoFrame; private readonly GUITickBox spectateBox; - + private readonly GUIFrame playerInfoContainer; private GUILayoutGroup infoContainer; private GUIComponent changesPendingText; + private bool createPendingChangesText = true; public GUIButton PlayerFrame; private readonly GUIComponent subPreviewContainer; @@ -103,11 +103,12 @@ namespace Barotrauma private GUIFrame characterInfoFrame; private GUIFrame appearanceFrame; - public GUIListBox HeadSelectionList; + public CharacterInfo.AppearanceCustomizationMenu CharacterAppearanceCustomizationMenu; public GUIFrame JobSelectionFrame; + public GUIFrame JobPreferenceContainer; public GUIListBox JobList; - + private float autoRestartTimer; //persistent characterinfo provided by the server @@ -142,7 +143,7 @@ namespace Barotrauma get; private set; } - + public GUITextBox ServerMessage { get; @@ -238,7 +239,7 @@ namespace Barotrauma get { return shuttleList.SelectedData as SubmarineInfo; } } - public CampaignSetupUI CampaignSetupUI; + public MultiPlayerCampaignSetupUI CampaignSetupUI; public List CampaignSubmarines = new List(); // Passed onto the gamesession when created @@ -340,7 +341,7 @@ namespace Barotrauma Stretch = true, RelativeSpacing = panelSpacing }; - + GUILayoutGroup panelHolder = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 1.0f), panelContainer.RectTransform)) { Stretch = true, @@ -413,7 +414,7 @@ namespace Barotrauma Stretch = true }; FileTransferProgressBar = new GUIProgressBar(new RectTransform(new Vector2(0.6f, 1.0f), fileTransferBottom.RectTransform), 0.0f, Color.DarkGreen); - FileTransferProgressText = new GUITextBlock(new RectTransform(Vector2.One, FileTransferProgressBar.RectTransform), "", + FileTransferProgressText = new GUITextBlock(new RectTransform(Vector2.One, FileTransferProgressBar.RectTransform), "", font: GUI.SmallFont, textAlignment: Alignment.CenterLeft); new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), fileTransferBottom.RectTransform), TextManager.Get("cancel"), style: "GUIButtonSmall") { @@ -541,7 +542,7 @@ namespace Barotrauma // Chat input - var chatRow = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.07f), socialHolder.RectTransform), + var chatRow = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.07f), socialHolder.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true @@ -604,7 +605,7 @@ namespace Barotrauma Font = GUI.SmallFont }; - roundControlsHolder = new GUILayoutGroup(new RectTransform(Vector2.One, bottomBarRight.RectTransform), + roundControlsHolder = new GUILayoutGroup(new RectTransform(Vector2.One, bottomBarRight.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true @@ -650,7 +651,7 @@ namespace Barotrauma return true; } }; - clientDisabledElements.Add(autoRestartBoxContainer); + clientDisabledElements.Add(autoRestartBoxContainer); //-------------------------------------------------------------------------------------------------------------------------------- //infoframe contents @@ -659,7 +660,7 @@ namespace Barotrauma //server info ------------------------------------------------------------------ // Server Info Header - GUILayoutGroup lobbyHeader = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), infoFrameContent.RectTransform), + GUILayoutGroup lobbyHeader = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), infoFrameContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { RelativeSpacing = 0.05f, @@ -813,7 +814,7 @@ namespace Barotrauma shuttleTickBox = new GUITickBox(new RectTransform(Vector2.One, shuttleHolder.RectTransform), TextManager.Get("RespawnShuttle")) { - Selected = true, + Selected = true, OnSelected = (GUITickBox box) => { GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, useRespawnShuttle: box.Selected); @@ -851,13 +852,13 @@ namespace Barotrauma //------------------------------------------------------------------------------------------------------------------ // Gamemode panel //------------------------------------------------------------------------------------------------------------------ - + GUILayoutGroup gameModeBackground = new GUILayoutGroup(new RectTransform(Vector2.One, gameModeContainer.RectTransform), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.01f }; - + GUILayoutGroup gameModeHolder = new GUILayoutGroup(new RectTransform(new Vector2(0.333f, 1.0f), gameModeBackground.RectTransform)) { Stretch = true @@ -874,7 +875,7 @@ namespace Barotrauma { OnSelected = VotableClicked }; - + foreach (GameModePreset mode in GameModePreset.List) { if (mode.IsSinglePlayer) { continue; } @@ -900,7 +901,7 @@ namespace Barotrauma modeTitle.State = modeDescription.State = c.State; }; - new GUIImage(new RectTransform(new Vector2(0.2f, 0.8f), modeFrame.RectTransform, Anchor.CenterLeft) { RelativeOffset = new Vector2(0.02f, 0.0f) }, + new GUIImage(new RectTransform(new Vector2(0.2f, 0.8f), modeFrame.RectTransform, Anchor.CenterLeft) { RelativeOffset = new Vector2(0.02f, 0.0f) }, style: "GameModeIcon." + mode.Identifier, scaleToFit: true); modeFrame.RectTransform.MinSize = new Point(0, (int)(modeContent.Children.Sum(c => c.Rect.Height + modeContent.AbsoluteSpacing) / modeContent.RectTransform.RelativeSize.Y)); @@ -928,17 +929,17 @@ namespace Barotrauma OnClicked = (_, __) => { CoroutineManager.StartCoroutine(WaitForStartRound(ContinueCampaignButton), "WaitForStartRound"); - GameMain.Client?.RequestStartRound(true); - return true; + GameMain.Client?.RequestStartRound(true); + return true; } }; QuitCampaignButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.3f), campaignContent.RectTransform), TextManager.Get("quitbutton"), textAlignment: Alignment.Center) { - OnClicked = (_, __) => + OnClicked = (_, __) => { - GameMain.Client.RequestSelectMode(modeList.Content.GetChildIndex(modeList.Content.GetChildByUserData(GameModePreset.Sandbox))); - return true; + GameMain.Client.RequestSelectMode(modeList.Content.GetChildIndex(modeList.Content.GetChildByUserData(GameModePreset.Sandbox))); + return true; } }; @@ -950,7 +951,7 @@ namespace Barotrauma Stretch = true }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), missionHolder.RectTransform) { MinSize = new Point(0, 25) }, + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), missionHolder.RectTransform) { MinSize = new Point(0, 25) }, TextManager.Get("MissionType"), font: GUI.SubHeadingFont); missionTypeList = new GUIListBox(new RectTransform(Vector2.One, missionHolder.RectTransform)) { @@ -1002,7 +1003,7 @@ namespace Barotrauma clientDisabledElements.AddRange(missionTypeTickBoxes); //------------------------------------------------------------------ - // settings panel + // settings panel //------------------------------------------------------------------ GUILayoutGroup settingsHolder = new GUILayoutGroup(new RectTransform(new Vector2(0.333f, 1.0f), gameModeBackground.RectTransform)) @@ -1083,7 +1084,7 @@ namespace Barotrauma } }; - traitorProbabilityText = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), traitorProbContainer.RectTransform), TextManager.Get("No"), + traitorProbabilityText = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), traitorProbContainer.RectTransform), TextManager.Get("No"), textAlignment: Alignment.Center, style: "GUITextBox"); traitorProbabilityButtons[1] = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), traitorProbContainer.RectTransform), style: "GUIButtonToggleRight") { @@ -1249,12 +1250,10 @@ namespace Barotrauma { chatInput.Deselect(); CampaignCharacterDiscarded = false; - HeadSelectionList = null; + + CharacterAppearanceCustomizationMenu?.Dispose(); JobSelectionFrame = null; - foreach (Sprite sprite in characterSprites) { sprite.Remove(); } - characterSprites.Clear(); - /*foreach (Sprite sprite in jobPreferenceSprites) { sprite.Remove(); } jobPreferenceSprites.Clear();*/ } @@ -1263,8 +1262,8 @@ namespace Barotrauma { if (GameMain.NetworkMember == null) { return; } - if (HeadSelectionList != null) { HeadSelectionList.Visible = false; } - if (JobSelectionFrame != null) { JobSelectionFrame.Visible = false; } + CharacterAppearanceCustomizationMenu?.Dispose(); + JobSelectionFrame = null; infoFrameContent.Recalculate(); @@ -1302,7 +1301,7 @@ namespace Barotrauma { spectateButton.Visible = false; } - SetSpectate(spectateBox.Selected); + SetSpectate(spectateBox.Selected); if (GameMain.Client != null) { @@ -1324,7 +1323,7 @@ namespace Barotrauma { publicOrPrivate.Text = isPublic ? TextManager.Get("PublicLobbyTag") : TextManager.Get("PrivateLobbyTag"); } - + public void RefreshEnabledElements() { ServerName.Readonly = !GameMain.Client.HasPermission(ClientPermissions.ManageSettings); @@ -1347,7 +1346,7 @@ namespace Barotrauma button.Enabled = CampaignSetupFrame.Visible && GameMain.Client.HasPermission(ClientPermissions.ManageSettings); } - traitorProbabilityButtons[0].Enabled = traitorProbabilityButtons[1].Enabled = traitorProbabilityText.Enabled = + traitorProbabilityButtons[0].Enabled = traitorProbabilityButtons[1].Enabled = traitorProbabilityText.Enabled = !CampaignFrame.Visible && !CampaignSetupFrame.Visible && GameMain.Client.HasPermission(ClientPermissions.ManageSettings); botCountButtons[0].Enabled = botCountButtons[1].Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); botSpawnModeButtons[0].Enabled = botSpawnModeButtons[1].Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); @@ -1360,7 +1359,7 @@ namespace Barotrauma ServerName.Readonly = !GameMain.Client.HasPermission(ClientPermissions.ManageSettings); ServerMessage.Readonly = !GameMain.Client.HasPermission(ClientPermissions.ManageSettings); shuttleTickBox.Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); - SubList.Enabled = !CampaignFrame.Visible && (GameMain.Client.ServerSettings.Voting.AllowSubVoting || GameMain.Client.HasPermission(ClientPermissions.SelectSub)); + SubList.Enabled = !CampaignFrame.Visible && (GameMain.Client.ServerSettings.Voting.AllowSubVoting || GameMain.Client.HasPermission(ClientPermissions.SelectSub)); shuttleList.Enabled = shuttleList.ButtonEnabled = GameMain.Client.HasPermission(ClientPermissions.SelectSub); ModeList.Enabled = GameMain.Client.ServerSettings.Voting.AllowModeVoting || GameMain.Client.HasPermission(ClientPermissions.SelectMode); LogButtons.Visible = GameMain.Client.HasPermission(ClientPermissions.ServerLog); @@ -1383,7 +1382,7 @@ namespace Barotrauma } public void SetCampaignCharacterInfo(CharacterInfo newCampaignCharacterInfo) - { + { if (newCampaignCharacterInfo != null) { if (CampaignCharacterDiscarded) { return; } @@ -1405,29 +1404,24 @@ namespace Barotrauma UpdatePlayerFrame(characterInfo, allowEditing, playerInfoContainer); } - public void CreatePlayerFrame(GUIComponent parent) + public void CreatePlayerFrame(GUIComponent parent, bool createPendingText = true, bool alwaysAllowEditing = false) { UpdatePlayerFrame( - Character.Controlled?.Info ?? playerInfoContainer.Children?.First().UserData as CharacterInfo, - allowEditing: campaignCharacterInfo == null, - parent: parent); + Character.Controlled?.Info ?? playerInfoContainer.Children?.First().UserData as CharacterInfo, + allowEditing: alwaysAllowEditing || campaignCharacterInfo == null, + parent: parent, + createPendingText: createPendingText); } - private void UpdatePlayerFrame(CharacterInfo characterInfo, bool allowEditing, GUIComponent parent) + private void UpdatePlayerFrame(CharacterInfo characterInfo, bool allowEditing, GUIComponent parent, bool createPendingText = true) { + createPendingChangesText = createPendingText; if (characterInfo == null || CampaignCharacterDiscarded) { characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, GameMain.Client.Name, null); - characterInfo.RecreateHead( - GameMain.Config.CharacterHeadIndex, - GameMain.Config.CharacterRace, - GameMain.Config.CharacterGender, - GameMain.Config.CharacterHairIndex, - GameMain.Config.CharacterBeardIndex, - GameMain.Config.CharacterMoustacheIndex, - GameMain.Config.CharacterFaceAttachmentIndex); + characterInfo.RecreateHead(GameMain.Config.PlayerCharacterCustomization); GameMain.Client.CharacterInfo = characterInfo; - characterInfo.OmitJobInPortraitClothing = true; + characterInfo.OmitJobInPortraitClothing = false; } parent.ClearChildren(); @@ -1436,9 +1430,9 @@ namespace Barotrauma infoContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, isGameRunning ? 0.95f : 0.9f), parent.RectTransform, Anchor.BottomCenter), childAnchor: Anchor.TopCenter) { - RelativeSpacing = 0.025f, + RelativeSpacing = 0.015f, Stretch = true, - UserData = characterInfo + UserData = characterInfo }; bool nameChangePending = isGameRunning && GameMain.Client.PendingName != string.Empty && GameMain.Client?.Character?.Name != GameMain.Client.PendingName; @@ -1454,6 +1448,7 @@ namespace Barotrauma CreateChangesPendingText(); } + CharacterNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.065f), infoContainer.RectTransform), !nameChangePending ? characterInfo.Name : GameMain.Client.PendingName, textAlignment: Alignment.Center) { MaxTextLength = Client.MaxNameLength, @@ -1477,7 +1472,10 @@ namespace Barotrauma { GameMain.Client.PendingName = tb.Text; TabMenu.PendingChanges = true; - CreateChangesPendingText(); + if (createPendingText) + { + CreateChangesPendingText(); + } } else { @@ -1485,15 +1483,12 @@ namespace Barotrauma } GameMain.Client.SetName(tb.Text); - }; + } }; - - new GUICustomComponent(new RectTransform(new Vector2(0.6f, 0.16f), infoContainer.RectTransform, Anchor.TopCenter), - onDraw: (sb, component) => characterInfo.DrawIcon(sb, component.Rect.Center.ToVector2(), targetAreaSize: component.Rect.Size.ToVector2())); if (allowEditing) { - GUILayoutGroup characterInfoTabs = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.04f), infoContainer.RectTransform), isHorizontal: true) + GUILayoutGroup characterInfoTabs = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.02f), infoContainer.RectTransform), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.02f @@ -1518,7 +1513,10 @@ namespace Barotrauma characterInfoFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.2f), infoContainer.RectTransform), style: null); characterInfoFrame.RectTransform.SizeChanged += RecalculateSubDescription; - JobList = new GUIListBox(new RectTransform(Vector2.One, characterInfoFrame.RectTransform), true) + 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)); + JobList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.6f), JobPreferenceContainer.RectTransform, Anchor.BottomCenter), true) { Enabled = true, OnSelected = (child, obj) => @@ -1554,7 +1552,7 @@ namespace Barotrauma }; } - UpdateJobPreferences(JobList); + UpdateJobPreferences(); appearanceFrame = new GUIFrame(new RectTransform(Vector2.One, characterInfoFrame.RectTransform), style: "GUIFrameListBox") { @@ -1564,6 +1562,8 @@ namespace Barotrauma } else { + characterInfo.CreateIcon(new RectTransform(new Vector2(0.6f, 0.16f), infoContainer.RectTransform, Anchor.TopCenter)); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContainer.RectTransform), characterInfo.Job.Name, textAlignment: Alignment.Center, font: GUI.SubHeadingFont, wrap: true) { HoverColor = Color.Transparent, @@ -1588,17 +1588,10 @@ namespace Barotrauma IgnoreLayoutGroups = true, OnClicked = (btn, userdata) => { - var confirmation = new GUIMessageBox(TextManager.Get("NewCampaignCharacterHeader"), TextManager.Get("NewCampaignCharacterText"), - new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); - confirmation.Buttons[0].OnClicked += confirmation.Close; - confirmation.Buttons[0].OnClicked += (btn2, userdata2) => + TryDiscardCampaignCharacter(() => { - CampaignCharacterDiscarded = true; - campaignCharacterInfo = null; UpdatePlayerFrame(null, true, parent); - return true; - }; - confirmation.Buttons[1].OnClicked += confirmation.Close; + }); return true; } }; @@ -1676,10 +1669,25 @@ namespace Barotrauma }; } } - + + public void TryDiscardCampaignCharacter(Action onYes) + { + var confirmation = new GUIMessageBox(TextManager.Get("NewCampaignCharacterHeader"), TextManager.Get("NewCampaignCharacterText"), + new[] { TextManager.Get("Yes"), TextManager.Get("No") }); + confirmation.Buttons[0].OnClicked += confirmation.Close; + confirmation.Buttons[0].OnClicked += (btn2, userdata2) => + { + CampaignCharacterDiscarded = true; + campaignCharacterInfo = null; + onYes(); + return true; + }; + confirmation.Buttons[1].OnClicked += confirmation.Close; + } + private void CreateChangesPendingText() { - if (changesPendingText != null || infoContainer == null) { return; } + if (!createPendingChangesText || changesPendingText != null || infoContainer == null) { return; } changesPendingText = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.065f), infoContainer.Parent.Parent.RectTransform, Anchor.BottomCenter, Pivot.TopCenter) { RelativeOffset = new Vector2(0f, -0.03f) }, style: "OuterGlow") @@ -1687,14 +1695,29 @@ namespace Barotrauma Color = Color.Black, IgnoreLayoutGroups = true }; - var text = new GUITextBlock(new RectTransform(Vector2.One, changesPendingText.RectTransform, Anchor.Center), + var text = new GUITextBlock(new RectTransform(Vector2.One, changesPendingText.RectTransform, Anchor.Center), TextManager.Get("tabmenu.characterchangespending"), textColor: GUI.Style.Orange, textAlignment: Alignment.Center, style: null); changesPendingText.RectTransform.MinSize = new Point((int)(text.TextSize.X * 1.2f), (int)(text.TextSize.Y * 2.0f)); } + public static void CreateChangesPendingFrame(GUIComponent parent) + { + parent.ClearChildren(); + GUIFrame changesPendingFrame = new GUIFrame(new RectTransform(Vector2.One, parent.RectTransform, Anchor.Center), + style: "OuterGlow") + { + Color = Color.Black + }; + new GUITextBlock(new RectTransform(Vector2.One, changesPendingFrame.RectTransform, Anchor.Center), + TextManager.Get("tabmenu.characterchangespending"), textColor: GUI.Style.Orange, textAlignment: Alignment.Center, style: null) + { + AutoScaleHorizontal = true + }; + } + private void CreateJobVariantTooltip(JobPrefab jobPrefab, int variant, GUIComponent parentSlot) { - jobVariantTooltip = new GUIFrame(new RectTransform(new Point((int)(400 * GUI.Scale), (int)(180 * GUI.Scale)), GUI.Canvas, pivot: Pivot.BottomRight), + jobVariantTooltip = new GUIFrame(new RectTransform(new Point((int)(400 * GUI.Scale), (int)(180 * GUI.Scale)), GUI.Canvas, pivot: Pivot.BottomRight), style: "GUIToolTip") { UserData = new Pair(jobPrefab, variant) @@ -1758,7 +1781,7 @@ namespace Barotrauma if (spectateBox.Selected && !allowSpectating) { spectateBox.Selected = false; } // Hide spectate tickbox if spectating is not allowed - spectateBox.Visible = allowSpectating; + spectateBox.Visible = allowSpectating; } public void SetAutoRestart(bool enabled, float timer = 0.0f) @@ -1777,7 +1800,7 @@ namespace Barotrauma if (subList == null) { return; } subList.ClearChildren(); - + foreach (SubmarineInfo sub in submarines) { AddSubmarine(subList, sub); @@ -1863,15 +1886,15 @@ namespace Barotrauma UserData = "classtext", TextColor = subTextBlock.TextColor * 0.8f, ToolTip = subTextBlock.RawToolTip - }; + }; } - + } public bool VotableClicked(GUIComponent component, object userData) { if (GameMain.Client == null) { return false; } - + VoteType voteType; if (component.Parent == GameMain.NetLobbyScreen.SubList.Content) { @@ -1963,7 +1986,7 @@ namespace Barotrauma return true; } - + public void AddPlayer(Client client) { GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), playerList.Content.RectTransform) { MinSize = new Point(0, (int)(30 * GUI.Scale)) }, @@ -1987,8 +2010,8 @@ namespace Barotrauma OverrideState = GUIComponent.ComponentState.None, HoverColor = Color.White }; - - new GUIImage(new RectTransform(new Point((int)(textBlock.Rect.Height * 0.8f)), textBlock.RectTransform, Anchor.CenterRight) { AbsoluteOffset = new Point(5, 0) }, + + new GUIImage(new RectTransform(new Point((int)(textBlock.Rect.Height * 0.8f)), textBlock.RectTransform, Anchor.CenterRight) { AbsoluteOffset = new Point(5, 0) }, "GUISoundIconDisabled") { UserData = "soundicondisabled", @@ -2119,16 +2142,16 @@ namespace Barotrauma { Stretch = true }; - - var nameText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), headerContainer.RectTransform), + + var nameText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), headerContainer.RectTransform), text: selectedClient.Name, font: GUI.LargeFont); nameText.Text = ToolBox.LimitString(nameText.Text, nameText.Font, (int)(nameText.Rect.Width * 0.95f)); if (hasManagePermissions) { PlayerFrame.UserData = selectedClient; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), paddedPlayerFrame.RectTransform), + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), paddedPlayerFrame.RectTransform), TextManager.Get("Rank"), font: GUI.SubHeadingFont); var rankDropDown = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.1f), paddedPlayerFrame.RectTransform), TextManager.Get("Rank")) @@ -2168,7 +2191,7 @@ namespace Barotrauma RelativeSpacing = 0.05f }; var permissionLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), permissionLabels.RectTransform), TextManager.Get("Permissions"), font: GUI.SubHeadingFont); - var consoleCommandLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), permissionLabels.RectTransform), + var consoleCommandLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), permissionLabels.RectTransform), TextManager.Get("PermittedConsoleCommands"), wrap: true, font: GUI.SubHeadingFont); GUITextBlock.AutoScaleAndNormalize(permissionLabel, consoleCommandLabel); @@ -2313,7 +2336,7 @@ namespace Barotrauma var buttonAreaTop = myClient ? null : new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.08f), paddedPlayerFrame.RectTransform), isHorizontal: true); var buttonAreaLower = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.08f), paddedPlayerFrame.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); - + if (!myClient) { if (GameMain.Client.HasPermission(ClientPermissions.Ban)) @@ -2337,7 +2360,7 @@ namespace Barotrauma if (GameMain.Client != null && GameMain.Client.ConnectedClients.Contains(selectedClient)) { - if (GameMain.Client.ServerSettings.Voting.AllowVoteKick && + if (GameMain.Client.ServerSettings.Voting.AllowVoteKick && selectedClient != null && selectedClient.AllowKicking) { var kickVoteButton = new GUIButton(new RectTransform(new Vector2(0.34f, 1.0f), buttonAreaLower.RectTransform), @@ -2404,7 +2427,7 @@ namespace Barotrauma } buttonAreaLower.RectTransform.NonScaledSize = new Point(buttonAreaLower.Rect.Width, buttonAreaLower.RectTransform.Children.Max(c => c.NonScaledSize.Y)); - + if (buttonAreaTop != null) { if (buttonAreaTop.CountChildren == 0) @@ -2437,7 +2460,7 @@ namespace Barotrauma public void KickPlayer(Client client) { if (GameMain.NetworkMember == null || client == null) { return; } - GameMain.Client.CreateKickReasonPrompt(client.Name, false); + GameMain.Client.CreateKickReasonPrompt(client.Name, false); } public void BanPlayer(Client client) @@ -2451,15 +2474,15 @@ namespace Barotrauma if (GameMain.NetworkMember == null || client == null) { return; } GameMain.Client.CreateKickReasonPrompt(client.Name, ban: true, rangeBan: true); } - + public override void AddToGUIUpdateList() { base.AddToGUIUpdateList(); - + //CampaignSetupUI?.AddToGUIUpdateList(); JobInfoFrame?.AddToGUIUpdateList(); - HeadSelectionList?.AddToGUIUpdateList(); + CharacterAppearanceCustomizationMenu?.AddToGUIUpdateList(); JobSelectionFrame?.AddToGUIUpdateList(); } @@ -2529,14 +2552,11 @@ namespace Barotrauma } } - if (HeadSelectionList != null && PlayerInput.PrimaryMouseButtonDown() && !GUI.IsMouseOn(HeadSelectionList)) - { - HeadSelectionList.Visible = false; - } + CharacterAppearanceCustomizationMenu?.Update(); if (JobSelectionFrame != null && PlayerInput.PrimaryMouseButtonDown() && !GUI.IsMouseOn(JobSelectionFrame)) { JobList.Deselect(); - JobSelectionFrame.Visible = false; + JobSelectionFrame.Visible = false; } if (GUI.MouseOn?.UserData is Pair jobPrefab && GUI.MouseOn.Style?.Name == "JobVariantButton") @@ -2562,7 +2582,7 @@ namespace Barotrauma GUI.DrawBackgroundSprite(spriteBatch, backgroundSprite); spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); - + GUI.Draw(Cam, spriteBatch); spriteBatch.End(); } @@ -2573,7 +2593,7 @@ namespace Barotrauma if (GameMain.NetworkMember?.ServerSettings == null) { return; } PlayStyle playStyle = GameMain.NetworkMember.ServerSettings.PlayStyle; - if ((int)playStyle < 0 || + if ((int)playStyle < 0 || (int)playStyle >= ServerListScreen.PlayStyleBanners.Length) { return; @@ -2698,9 +2718,9 @@ namespace Barotrauma msg.RectTransform.SizeChanged += Recalculate; } - if ((prevSize == 1.0f && chatBox.BarScroll == 0.0f) || (prevSize < 1.0f && chatBox.BarScroll == 1.0f)) - { - chatBox.BarScroll = 1.0f; + if ((prevSize == 1.0f && chatBox.BarScroll == 0.0f) || (prevSize < 1.0f && chatBox.BarScroll == 1.0f)) + { + chatBox.BarScroll = 1.0f; } } @@ -2709,212 +2729,56 @@ namespace Barotrauma jobPreferencesButton.Selected = true; appearanceButton.Selected = false; - JobList.Visible = true; + JobPreferenceContainer.Visible = true; appearanceFrame.Visible = false; return false; } - private bool SelectAppearanceTab(GUIButton button, object userData) + private bool SelectAppearanceTab(GUIButton button, object _) { jobPreferencesButton.Selected = false; appearanceButton.Selected = true; - JobList.Visible = false; + JobPreferenceContainer.Visible = false; appearanceFrame.Visible = true; appearanceFrame.ClearChildren(); - if (HeadSelectionList != null) { HeadSelectionList.Visible = false; } - - GUIButton maleButton = null; - GUIButton femaleButton = null; var info = GameMain.Client.CharacterInfo; - - GUILayoutGroup content = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), appearanceFrame.RectTransform, Anchor.Center)) + CharacterAppearanceCustomizationMenu = new CharacterInfo.AppearanceCustomizationMenu(info, appearanceFrame) { - RelativeSpacing = 0.05f - }; - - Vector2 elementSize = new Vector2(1.0f, 0.18f); - - GUILayoutGroup genderContainer = new GUILayoutGroup(new RectTransform(elementSize, content.RectTransform), isHorizontal: true) - { - Stretch = true, - RelativeSpacing = 0.05f - }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), genderContainer.RectTransform), TextManager.Get("Gender"), font: GUI.SubHeadingFont); - maleButton = new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), genderContainer.RectTransform), - TextManager.Get("Male"), style: "ListBoxElement") - { - UserData = Gender.Male, - OnClicked = OpenHeadSelection, - Selected = info.Gender == Gender.Male - }; - femaleButton = new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), genderContainer.RectTransform), - TextManager.Get("Female"), style: "ListBoxElement") - { - UserData = Gender.Female, - OnClicked = OpenHeadSelection, - Selected = info.Gender == Gender.Female - }; - - int hairCount = info.FilterByTypeAndHeadID(info.FilterElementsByGenderAndRace(info.Wearables, info.Head.gender, info.Head.race), WearableType.Hair, info.HeadSpriteId).Count(); - if (hairCount > 0) - { - var label = new GUITextBlock(new RectTransform(elementSize, content.RectTransform), TextManager.Get("FaceAttachment.Hair"), font: GUI.SubHeadingFont); - var hairSlider = new GUIScrollBar(new RectTransform(new Vector2(0.4f, 1.0f), label.RectTransform, Anchor.CenterRight), style: "GUISlider") + OnHeadSwitch = menu => { - Range = new Vector2(0, hairCount), - StepValue = 1, - BarScrollValue = info.HairIndex, - OnMoved = SwitchHair, - OnReleased = SaveHead, - BarSize = 1.0f / (float)(hairCount + 1) - }; - } - - int beardCount = info.FilterByTypeAndHeadID(info.FilterElementsByGenderAndRace(info.Wearables, info.Head.gender, info.Head.race), WearableType.Beard, info.HeadSpriteId).Count(); - if (beardCount > 0) - { - var label = new GUITextBlock(new RectTransform(elementSize, content.RectTransform), TextManager.Get("FaceAttachment.Beard"), font: GUI.SubHeadingFont); - var beardSlider = new GUIScrollBar(new RectTransform(new Vector2(0.4f, 1.0f), label.RectTransform, Anchor.CenterRight), style: "GUISlider") + StoreHead(true); + UpdateJobPreferences(); + SelectAppearanceTab(button, _); + }, + OnSliderMoved = (bar, scroll) => { - Range = new Vector2(0, beardCount), - StepValue = 1, - BarScrollValue = info.BeardIndex, - OnMoved = SwitchBeard, - OnReleased = SaveHead, - BarSize = 1.0f / (float)(beardCount + 1) - }; - } - - int moustacheCount = info.FilterByTypeAndHeadID(info.FilterElementsByGenderAndRace(info.Wearables, info.Head.gender, info.Head.race), WearableType.Moustache, info.HeadSpriteId).Count(); - if (moustacheCount > 0) - { - var label = new GUITextBlock(new RectTransform(elementSize, content.RectTransform), TextManager.Get("FaceAttachment.Moustache"), font: GUI.SubHeadingFont); - var moustacheSlider = new GUIScrollBar(new RectTransform(new Vector2(0.4f, 1.0f), label.RectTransform, Anchor.CenterRight), style: "GUISlider") - { - Range = new Vector2(0, moustacheCount), - StepValue = 1, - BarScrollValue = info.MoustacheIndex, - OnMoved = SwitchMoustache, - OnReleased = SaveHead, - BarSize = 1.0f / (float)(moustacheCount + 1) - }; - } - - int faceAttachmentCount = info.FilterByTypeAndHeadID(info.FilterElementsByGenderAndRace(info.Wearables, info.Head.gender, info.Head.race), WearableType.FaceAttachment, info.HeadSpriteId).Count(); - if (faceAttachmentCount > 0) - { - var label = new GUITextBlock(new RectTransform(elementSize, content.RectTransform), TextManager.Get("FaceAttachment.Accessories"), font: GUI.SubHeadingFont); - var faceAttachmentSlider = new GUIScrollBar(new RectTransform(new Vector2(0.4f, 1.0f), label.RectTransform, Anchor.CenterRight), style: "GUISlider") - { - Range = new Vector2(0, faceAttachmentCount), - StepValue = 1, - BarScrollValue = info.FaceAttachmentIndex, - OnMoved = SwitchFaceAttachment, - OnReleased = SaveHead, - BarSize = 1.0f / (float)(faceAttachmentCount + 1) - }; - } + StoreHead(false); + return false; + }, + OnSliderReleased = SaveHead + }; return false; } - - private bool OpenHeadSelection(GUIButton button, object userData) + + private bool SaveHead(GUIScrollBar scrollBar, float barScroll) => StoreHead(true); + private bool StoreHead(bool save) { - Gender selectedGender = (Gender)userData; - if (HeadSelectionList != null) + GameMain.Config.PlayerCharacterCustomization = GameMain.Client.CharacterInfo.Head; + if (save) { - HeadSelectionList.Visible = true; - foreach (GUIComponent child in HeadSelectionList.Content.Children) + if (GameMain.GameSession?.IsRunning ?? false) { - child.Visible = (Gender)child.UserData == selectedGender; - child.Children.ForEach(c => c.Visible = ((Tuple)c.UserData).Item1 == selectedGender); + TabMenu.PendingChanges = true; + CreateChangesPendingText(); } - return true; + GameMain.Config.SaveNewPlayerConfig(); } - - var info = GameMain.Client.CharacterInfo; - - HeadSelectionList = new GUIListBox( - new RectTransform(new Point(characterInfoFrame.Rect.Width, (characterInfoFrame.Rect.Bottom - button.Rect.Bottom) + characterInfoFrame.Rect.Height * 2), GUI.Canvas) - { - AbsoluteOffset = new Point(characterInfoFrame.Rect.Right - characterInfoFrame.Rect.Width, button.Rect.Bottom) - }); - - characterInfoFrame.RectTransform.SizeChanged += () => - { - if (characterInfoFrame == null || HeadSelectionList?.RectTransform == null || button == null) { return; } - HeadSelectionList.RectTransform.Resize(new Point(characterInfoFrame.Rect.Width, (characterInfoFrame.Rect.Bottom - button.Rect.Bottom) + characterInfoFrame.Rect.Height * 2)); - HeadSelectionList.RectTransform.AbsoluteOffset = new Point(characterInfoFrame.Rect.Right - characterInfoFrame.Rect.Width, button.Rect.Bottom); - if (SelectedSub != null) { CreateSubPreview(SelectedSub); } - }; - - new GUIFrame(new RectTransform(new Vector2(1.25f, 1.25f), HeadSelectionList.RectTransform, Anchor.Center), style: "OuterGlow", color: Color.Black) - { - UserData = "outerglow", - CanBeFocused = false - }; - - GUILayoutGroup row = null; - int itemsInRow = 0; - - XElement headElement = info.Ragdoll.MainElement.Elements().FirstOrDefault(e => e.GetAttributeString("type", "").Equals("head", StringComparison.OrdinalIgnoreCase)); - XElement headSpriteElement = headElement.Element("sprite"); - string spritePathWithTags = headSpriteElement.Attribute("texture").Value; - - var characterConfigElement = info.CharacterConfigElement; - - var heads = info.Heads; - if (heads != null) - { - row = null; - itemsInRow = 0; - foreach (var head in heads) - { - var headPreset = head.Key; - Gender gender = headPreset.Gender; - Race race = headPreset.Race; - int headIndex = headPreset.ID; - - string spritePath = spritePathWithTags - .Replace("[GENDER]", gender.ToString().ToLowerInvariant()) - .Replace("[RACE]", race.ToString().ToLowerInvariant()); - - if (!File.Exists(spritePath)) { continue; } - - Sprite headSprite = new Sprite(headSpriteElement, "", spritePath); - headSprite.SourceRect = new Rectangle(CharacterInfo.CalculateOffset(headSprite, head.Value.ToPoint()), headSprite.SourceRect.Size); - characterSprites.Add(headSprite); - - if (itemsInRow >= 4 || row == null || gender != (Gender)row.UserData) - { - row = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.333f), HeadSelectionList.Content.RectTransform), true) - { - UserData = gender, - Visible = gender == selectedGender - }; - itemsInRow = 0; - } - - var btn = new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), row.RectTransform), style: "ListBoxElementSquare") - { - OutlineColor = Color.White * 0.5f, - PressedColor = Color.White * 0.5f, - UserData = new Tuple(gender, race, headIndex), - OnClicked = SwitchHead, - Selected = gender == info.Gender && race == info.Race && headIndex == info.HeadSpriteId, - Visible = gender == selectedGender - }; - - new GUIImage(new RectTransform(Vector2.One, btn.RectTransform), headSprite, scaleToFit: true); - itemsInRow++; - } - } - - return false; + return true; } private bool SwitchJob(GUIButton _, object obj) @@ -2947,7 +2811,7 @@ namespace Barotrauma } } - UpdateJobPreferences(JobList); + UpdateJobPreferences(); if (moveToNext) { @@ -2978,14 +2842,14 @@ namespace Barotrauma return true; } - Point frameSize = new Point(characterInfoFrame.Rect.Width, characterInfoFrame.Rect.Height * 2); + Point frameSize = new Point(characterInfoFrame.Rect.Width, (int)(characterInfoFrame.Rect.Height * 2 * 0.6f)); JobSelectionFrame = new GUIFrame(new RectTransform(frameSize, GUI.Canvas, Anchor.TopLeft) { AbsoluteOffset = new Point(characterInfoFrame.Rect.Right - frameSize.X, characterInfoFrame.Rect.Bottom) }, style:"GUIFrameListBox"); characterInfoFrame.RectTransform.SizeChanged += () => { if (characterInfoFrame == null || JobSelectionFrame?.RectTransform == null) { return; } - Point size = new Point(characterInfoFrame.Rect.Width, characterInfoFrame.Rect.Height * 2); + Point size = new Point(characterInfoFrame.Rect.Width, (int)(characterInfoFrame.Rect.Height * 2 * 0.6f)); JobSelectionFrame.RectTransform.Resize(size); JobSelectionFrame.RectTransform.AbsoluteOffset = new Point(characterInfoFrame.Rect.Right - size.X, characterInfoFrame.Rect.Bottom); }; @@ -3102,7 +2966,7 @@ namespace Barotrauma { Pair sprite = outfitPreview.Sprites[j]; float aspectRatio = outfitPreview.Dimensions.Y / outfitPreview.Dimensions.X; - retVal[i][j] = new GUIImage(new RectTransform(new Vector2(0.7f / aspectRatio, 0.7f), innerFrame.RectTransform, Anchor.Center) + retVal[i][j] = new GUIImage(new RectTransform(new Vector2(0.7f / aspectRatio, 0.7f), innerFrame.RectTransform, Anchor.Center) { RelativeOffset = sprite.Second / outfitPreview.Dimensions }, sprite.First, scaleToFit: true) { PressedColor = Color.White, @@ -3141,88 +3005,6 @@ namespace Barotrauma return retVal; } - private bool SwitchHead(GUIButton button, object obj) - { - var info = GameMain.Client.CharacterInfo; - - Gender gender = ((Tuple)obj).Item1; - Race race = ((Tuple)obj).Item2; - int id = ((Tuple)obj).Item3; - - if (gender != info.Gender || race != info.Race || id != info.HeadSpriteId) - { - info.Head = new CharacterInfo.HeadInfo(id, gender, race); - info.ReloadHeadAttachments(); - } - StoreHead(true); - - UpdateJobPreferences(JobList); - - SelectAppearanceTab(button, obj); - - return true; - } - - private bool SaveHead(GUIScrollBar scrollBar, float barScroll) => StoreHead(true); - private bool SwitchHair(GUIScrollBar scrollBar, float barScroll) => SwitchAttachment(scrollBar, WearableType.Hair); - private bool SwitchBeard(GUIScrollBar scrollBar, float barScroll) => SwitchAttachment(scrollBar, WearableType.Beard); - private bool SwitchMoustache(GUIScrollBar scrollBar, float barScroll) => SwitchAttachment(scrollBar, WearableType.Moustache); - private bool SwitchFaceAttachment(GUIScrollBar scrollBar, float barScroll) => SwitchAttachment(scrollBar, WearableType.FaceAttachment); - private bool SwitchAttachment(GUIScrollBar scrollBar, WearableType type) - { - var info = GameMain.Client.CharacterInfo; - int index = (int)scrollBar.BarScrollValue; - switch (type) - { - case WearableType.Beard: - info.Head = new CharacterInfo.HeadInfo(info.HeadSpriteId, info.Gender, info.Race, info.HairIndex, index, info.MoustacheIndex, info.FaceAttachmentIndex); - break; - case WearableType.FaceAttachment: - info.Head = new CharacterInfo.HeadInfo(info.HeadSpriteId, info.Gender, info.Race, info.HairIndex, info.BeardIndex, info.MoustacheIndex, index); - break; - case WearableType.Hair: - info.Head = new CharacterInfo.HeadInfo(info.HeadSpriteId, info.Gender, info.Race, index, info.BeardIndex, info.MoustacheIndex, info.FaceAttachmentIndex); - break; - case WearableType.Moustache: - info.Head = new CharacterInfo.HeadInfo(info.HeadSpriteId, info.Gender, info.Race, info.HairIndex, info.BeardIndex, index, info.FaceAttachmentIndex); - break; - default: - DebugConsole.ThrowError($"Wearable type not implemented: {type}"); - return false; - } - info.ReloadHeadAttachments(); - StoreHead(false); - return true; - } - - private bool StoreHead(bool save) - { - var info = GameMain.Client.CharacterInfo; - var config = GameMain.Config; - - config.CharacterRace = info.Race; - config.CharacterGender = info.Gender; - config.CharacterHeadIndex = info.HeadSpriteId; - config.CharacterHairIndex = info.HairIndex; - config.CharacterBeardIndex = info.BeardIndex; - config.CharacterMoustacheIndex = info.MoustacheIndex; - config.CharacterFaceAttachmentIndex = info.FaceAttachmentIndex; - - if (save) - { - if (GameMain.GameSession?.IsRunning ?? false) - { - TabMenu.PendingChanges = true; - CreateChangesPendingText(); - } - - GameMain.Config.SaveNewPlayerConfig(); - } - - return true; - } - - public void SelectMode(int modeIndex) { if (modeIndex < 0 || modeIndex >= modeList.Content.CountChildren) { return; } @@ -3327,10 +3109,10 @@ namespace Barotrauma ReadyToStartBox.Parent.Visible = !GameMain.Client.GameStarted; - StartButton.Visible = - GameMain.Client.HasPermission(ClientPermissions.ManageRound) && - !GameMain.Client.GameStarted && - !CampaignSetupFrame.Visible && + StartButton.Visible = + GameMain.Client.HasPermission(ClientPermissions.ManageRound) && + !GameMain.Client.GameStarted && + !CampaignSetupFrame.Visible && !CampaignFrame.Visible; } @@ -3395,7 +3177,7 @@ namespace Barotrauma OnClicked = CloseJobInfo }; JobInfoFrame.OnClicked = (btn, userdata) => { if (GUI.MouseOn == btn || GUI.MouseOn == btn.TextBlock) CloseJobInfo(btn, userdata); return true; }; - + return true; } @@ -3405,8 +3187,13 @@ namespace Barotrauma return true; } - private void UpdateJobPreferences(GUIListBox listBox) + private void UpdateJobPreferences() { + GUICustomComponent characterIcon = JobPreferenceContainer.GetChild(); + JobPreferenceContainer.RemoveChild(characterIcon); + GameMain.Client.CharacterInfo.CreateIcon(new RectTransform(new Vector2(1.0f, 0.4f), JobPreferenceContainer.RectTransform, Anchor.TopCenter)); + + GUIListBox listBox = JobPreferenceContainer.GetChild(); /*foreach (Sprite sprite in jobPreferenceSprites) { sprite.Remove(); } jobPreferenceSprites.Clear();*/ @@ -3437,7 +3224,7 @@ namespace Barotrauma variantButton.OnClicked = (btn, obj) => { btn.Parent.UserData = obj; - UpdateJobPreferences(listBox); + UpdateJobPreferences(); return false; }; } @@ -3448,7 +3235,7 @@ namespace Barotrauma style: "GUIButtonInfo") { UserData = jobPrefab, - OnClicked = ViewJobInfo + OnClicked = ViewJobInfo }; // Remove button @@ -3544,7 +3331,7 @@ namespace Barotrauma .UserData as SubmarineInfo; //matching sub found and already selected, all good - if (sub != null) + if (sub != null) { if (subList == this.subList) { @@ -3583,7 +3370,7 @@ namespace Barotrauma FailedSelectedSub = null; else FailedSelectedShuttle = null; - + //hashes match, all good if (sub.MD5Hash?.Hash == md5Hash && SubmarineInfo.SavedSubmarines.Contains(sub)) { @@ -3593,7 +3380,7 @@ namespace Barotrauma //------------------------------------------------------------------------------------- //if we get to this point, a matching sub was not found or it has an incorrect MD5 hash - + if (subList == SubList) FailedSelectedSub = new Pair(subName, md5Hash); else @@ -3612,7 +3399,7 @@ namespace Barotrauma } else { - errorMsg = TextManager.GetWithVariables("SubDoesntMatchError", new string[3] { "[subname]" , "[myhash]", "[serverhash]" }, + errorMsg = TextManager.GetWithVariables("SubDoesntMatchError", new string[3] { "[subname]" , "[myhash]", "[serverhash]" }, new string[3] { sub.Name, sub.MD5Hash.ShortHash, Md5Hash.GetShortHash(md5Hash) }) + " "; } @@ -3645,7 +3432,7 @@ namespace Barotrauma { new GUIMessageBox(TextManager.Get("DownloadSubLabel"), errorMsg); } - return false; + return false; } public bool CheckIfCampaignSubMatches(SubmarineInfo serverSubmarine, string deliveryData) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs index 143a70d25..5a9c265f1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs @@ -41,21 +41,13 @@ namespace Barotrauma public Sprite(Texture2D texture, Rectangle? sourceRectangle, Vector2? newOffset, float newRotation = 0.0f, string path = null) { this.texture = texture; - sourceRect = sourceRectangle ?? new Rectangle(0, 0, texture.Width, texture.Height); - offset = newOffset ?? Vector2.Zero; - size = new Vector2(sourceRect.Width, sourceRect.Height); - origin = Vector2.Zero; - effects = SpriteEffects.None; - rotation = newRotation; - FilePath = path; - AddToList(this); } @@ -85,7 +77,7 @@ namespace Barotrauma EnsureLazyLoaded(isAsync: true); } - public void EnsureLazyLoaded(bool isAsync=false) + public void EnsureLazyLoaded(bool isAsync = false) { if (!LazyLoad || texture != null || cannotBeLoaded || loadingAsync) { return; } loadingAsync = isAsync; @@ -382,7 +374,7 @@ namespace Barotrauma { foreach (Sprite s in LoadedSprites) { - if (s.FullPath == FullPath) return; + if (s.FullPath == FullPath) { return; } } } } diff --git a/Barotrauma/BarotraumaClient/Content/Effects/thresholdtint.xnb b/Barotrauma/BarotraumaClient/Content/Effects/thresholdtint.xnb new file mode 100644 index 0000000000000000000000000000000000000000..e9784b881b0c63d9f9270e3ff60ee5e27df1b5dc GIT binary patch literal 1501 zcmZWp%}*0i5TCaTQpC`v#KZ)Hd+=nMMk6?$=%>3TleOoIm?Do^Iej>4-H#Qu* zY1iF!&C=63M}HxAZD%{3wJld#ZhEO+mm6+b>YLI@X?feSmjUSlQ`(wznucx7-bvq0 zkEh2cr&3zBWx6d#&RVkNI=Y$CO09L%*mx-StM<0EX04WK5{oE+M1DC}i;3g;_iqIy zNab)JDKQdM>LXgLEo5(f9CE%7{VW|!xnF<(_*NIda1KAZ4K)$?jKb>(%-hz~1bkLf zI-NgQW}w1HvNeMlH^V66LXe^;@EH>d>TaMKo<|fvjch?`WFJy2eiX(JMUSn(#-_kV z^#rv~iM|{yz!4PpeGzN>z7qNr`j~@RZBK!Z!Kfeh6dFa0j)Q(`zpnJpQv++{VLdprvlsQ5LF&5mM1WL4h0{ekI^%C zpuq~x<_Yt9p78z1<}-_Ilv3>)xa|a()zz-gxL%*J?62dIpCuCh*|VL2%{hr#XIjvw z0iM_}EfEc#;T>Vgg}B_Sz>jz#HRu4t^Kwd%yX0x!JQsyBsZp#K!`QVqT9l@Y_J5CD zvfGpK5f|p#%Y}2y#b1ercn-hQ;No*&yfTMB^6qz)Xp(4vu`Cg9Ub!~Id(yf&?#(%W zcDP5>$1+iUcs8HjX+Q=N@%3vX9L|n=FkfDoLo&=whwEjmT*JB7TEKy0EYsn0yNB0f zrLtV$OU-;^&=Qn|YdOXehd%YK;E&^+k0Y^uAZR_ zxe0bmk3}6hPv`McvI=%U;>v~gG?QF}n>-CE9QkHesu)7F#;L6?tL*yQR8^f*x oiM^^}xp}?0eIYTt7qt2(EJn_`aapCopJfvLCB4}kC){A7zd04)^e;(ELocMIeX^J%zAIWoOn&5n}itIP`Fw}%8$((>6qu*3Hd_ldvl;< z%w=qz`962!EADt)cj-_`bpezIVwZm9Iuc6ud*(aSGOhO?x^$2PaiV!&@+8(y(508j zT_D^~JR7NdF8eYG0)mh6XP1!S#re%s^2=|0H}+@IIFZ6vI*i71?jAGJK-xapp~N7+ zVR0yYkNfoE^Z4w#-I{6c3QSK!?ujIvQ9n>l{Bav1Bz`~S(i48W)o!g4QOrXhC;ryE z1#$^#R$o~4gdaa29ZFpCTvhyYsAxmQrbfgwKl}Cse;xJHDGiSjN#Z# z@?lsv~nVDv^j|Ikg_!>jkHq#VRSQ$AzV3P76`8E_^Uf7;v+RkV&(hSV#%+ZSf~d^WAAx- uf|TOmtW@{*(-B-2Lnpd_1iVNKskP6>LFEm%T~G0@Bu4Yni6V>qMbUqMuB8V6 literal 0 HcmV?d00001 diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 8bdd4831b..cf1f1eb9d 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.1500.3.0 + 0.1500.4.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 4dd212c17..af5c2accc 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.1500.3.0 + 0.1500.4.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/Shaders/Content.mgcb b/Barotrauma/BarotraumaClient/Shaders/Content.mgcb index e88794f28..7d48e26be 100644 --- a/Barotrauma/BarotraumaClient/Shaders/Content.mgcb +++ b/Barotrauma/BarotraumaClient/Shaders/Content.mgcb @@ -67,6 +67,12 @@ /processorParam:DebugMode=Auto /build:grainshader.fx +#begin thresholdtint.fx +/importer:EffectImporter +/processor:EffectProcessor +/processorParam:DebugMode=Auto +/build:thresholdtint.fx + #begin blueprintshader.fx /importer:EffectImporter /processor:EffectProcessor diff --git a/Barotrauma/BarotraumaClient/Shaders/Content_opengl.mgcb b/Barotrauma/BarotraumaClient/Shaders/Content_opengl.mgcb index 16d516848..82d54dedf 100644 --- a/Barotrauma/BarotraumaClient/Shaders/Content_opengl.mgcb +++ b/Barotrauma/BarotraumaClient/Shaders/Content_opengl.mgcb @@ -72,3 +72,9 @@ /processor:EffectProcessor /processorParam:DebugMode=Auto /build:blueprintshader_opengl.fx + +#begin thresholdtint_opengl.fx +/importer:EffectImporter +/processor:EffectProcessor +/processorParam:DebugMode=Auto +/build:thresholdtint_opengl.fx diff --git a/Barotrauma/BarotraumaClient/Shaders/thresholdtint.fx b/Barotrauma/BarotraumaClient/Shaders/thresholdtint.fx new file mode 100644 index 000000000..b70d04055 --- /dev/null +++ b/Barotrauma/BarotraumaClient/Shaders/thresholdtint.fx @@ -0,0 +1,32 @@ +Texture2D xBaseTexture; +sampler BaseTextureSampler = sampler_state { Texture = ; }; +Texture2D xTintMaskTexture; +sampler TintMaskTextureSampler = sampler_state { Texture = ; }; +Texture2D xCutoffTexture; +sampler CutoffTextureSampler = sampler_state { Texture = ; }; + +float highlightThreshold; +float highlightMultiplier; + +float baseToCutoffSizeRatio; + +float4 mainPS(float4 position : SV_POSITION, float4 clr : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +{ + float4 baseSample = xBaseTexture.Sample(BaseTextureSampler, texCoord); + float3 tintMaskSample = xTintMaskTexture.Sample(TintMaskTextureSampler, texCoord).rgb; + float cutoffSample = xCutoffTexture.Sample(CutoffTextureSampler, texCoord * baseToCutoffSizeRatio).r; + + float3 highlight = saturate((baseSample.rgb - (highlightThreshold * float3(1,1,1))) * highlightMultiplier); + float3 tinted = saturate(baseSample.rgb * clr.rgb + highlight); + return float4( + (tinted * tintMaskSample) + (baseSample.rgb * (float3(1,1,1) - tintMaskSample)), + baseSample.a * cutoffSample * clr.a); +} + +technique ThresholdTintShader +{ + pass Pass1 + { + PixelShader = compile ps_4_0_level_9_1 mainPS(); + } +} diff --git a/Barotrauma/BarotraumaClient/Shaders/thresholdtint_opengl.fx b/Barotrauma/BarotraumaClient/Shaders/thresholdtint_opengl.fx new file mode 100644 index 000000000..ff693a035 --- /dev/null +++ b/Barotrauma/BarotraumaClient/Shaders/thresholdtint_opengl.fx @@ -0,0 +1,32 @@ +Texture2D xBaseTexture; +sampler BaseTextureSampler = sampler_state { Texture = ; }; +Texture2D xTintMaskTexture; +sampler TintMaskTextureSampler = sampler_state { Texture = ; }; +Texture2D xCutoffTexture; +sampler CutoffTextureSampler = sampler_state { Texture = ; }; + +float highlightThreshold; +float highlightMultiplier; + +float baseToCutoffSizeRatio; + +float4 mainPS(float4 position : SV_POSITION, float4 clr : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +{ + float4 baseSample = tex2D(BaseTextureSampler, texCoord); + float3 tintMaskSample = tex2D(TintMaskTextureSampler, texCoord).rgb; + float cutoffSample = tex2D(CutoffTextureSampler, texCoord * baseToCutoffSizeRatio).r; + + float3 highlight = saturate((baseSample.rgb - (highlightThreshold * float3(1,1,1))) * highlightMultiplier); + float3 tinted = saturate(baseSample.rgb * clr.rgb + highlight); + return float4( + (tinted * tintMaskSample) + (baseSample.rgb * (float3(1,1,1) - tintMaskSample)), + baseSample.a * cutoffSample * clr.a); +} + +technique ThresholdTintShader +{ + pass Pass1 + { + PixelShader = compile ps_2_0 mainPS(); + } +} diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 5df6359c5..70656497c 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.1500.3.0 + 0.1500.4.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 5d6dfdf6e..d0a633e79 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.1500.3.0 + 0.1500.4.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 26fce29e3..19f20cd27 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.1500.3.0 + 0.1500.4.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 ac69ac86c..3729108f9 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs @@ -39,10 +39,13 @@ namespace Barotrauma msg.Write((byte)Gender); msg.Write((byte)Race); msg.Write((byte)HeadSpriteId); - msg.Write((byte)Head.HairIndex); - msg.Write((byte)Head.BeardIndex); - msg.Write((byte)Head.MoustacheIndex); - msg.Write((byte)Head.FaceAttachmentIndex); + msg.Write((byte)HairIndex); + msg.Write((byte)BeardIndex); + msg.Write((byte)MoustacheIndex); + msg.Write((byte)FaceAttachmentIndex); + msg.WriteColorR8G8B8(SkinColor); + msg.WriteColorR8G8B8(HairColor); + msg.WriteColorR8G8B8(FacialHairColor); msg.Write(ragdollFileName); if (Job != null) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MineralMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MineralMission.cs index b6555cb25..8be6f27df 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MineralMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/MineralMission.cs @@ -12,10 +12,10 @@ namespace Barotrauma msg.Write((byte)(Level.Loaded == null || !Level.Loaded.Caves.Contains(cave) ? 255 : Level.Loaded.Caves.IndexOf(cave))); } - foreach (var kvp in SpawnedResources) + foreach (var kvp in spawnedResources) { msg.Write((byte)kvp.Value.Count); - var rotation = ResourceClusters[kvp.Key].Second; + var rotation = resourceClusters[kvp.Key].rotation; msg.Write(rotation); foreach (var r in kvp.Value) { @@ -23,7 +23,7 @@ namespace Barotrauma } } - foreach (var kvp in RelevantLevelResources) + foreach (var kvp in relevantLevelResources) { msg.Write(kvp.Key); msg.Write((byte)kvp.Value.Length); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs index 19f03f8c3..129d6e622 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs @@ -39,6 +39,7 @@ namespace Barotrauma.Items.Components msg.Write(deteriorateAlwaysResetTimer); msg.Write(DeteriorateAlways); msg.Write(tinkeringDuration); + msg.Write(tinkeringStrength); msg.Write(CurrentFixer == null ? (ushort)0 : CurrentFixer.ID); msg.WriteRangedInteger((int)currentFixerAction, 0, 2); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 575f5c157..cedb86f86 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -3464,15 +3464,15 @@ namespace Barotrauma.Networking } catch (Exception e) { - //gender = Gender.Male; - //race = Race.White; - //headSpriteId = 0; DebugConsole.Log("Received invalid characterinfo from \"" + sender.Name + "\"! { " + e.Message + " }"); } int hairIndex = message.ReadByte(); int beardIndex = message.ReadByte(); int moustacheIndex = message.ReadByte(); int faceAttachmentIndex = message.ReadByte(); + Color skinColor = message.ReadColorR8G8B8(); + Color hairColor = message.ReadColorR8G8B8(); + Color facialHairColor = message.ReadColorR8G8B8(); List> jobPreferences = new List>(); int count = message.ReadByte(); @@ -3489,6 +3489,9 @@ namespace Barotrauma.Networking sender.CharacterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, sender.Name); sender.CharacterInfo.RecreateHead(headSpriteId, race, gender, hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex); + sender.CharacterInfo.SkinColor = skinColor; + sender.CharacterInfo.HairColor = hairColor; + sender.CharacterInfo.FacialHairColor = facialHairColor; if (jobPreferences.Count > 0) { diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 6b4075d6e..ab6d66a0c 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.1500.3.0 + 0.1500.4.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 a7a1728a0..3a605f435 100644 --- a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml +++ b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml @@ -118,6 +118,7 @@ + diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs index 319c68580..03d672ca5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs @@ -464,7 +464,7 @@ namespace Barotrauma }; break; case "return": - newObjective = new AIObjectiveReturn(character, this, priorityModifier: priorityModifier); + newObjective = new AIObjectiveReturn(character, orderGiver, this, priorityModifier: priorityModifier); newObjective.Abandoned += () => DismissSelf(order, option); newObjective.Completed += () => DismissSelf(order, option); break; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs index e9e310f16..66cc99a4a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs @@ -408,7 +408,7 @@ namespace Barotrauma } bool isCompleted = AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, targetCharacter) || - targetCharacter.CharacterHealth.GetAllAfflictions().All(a => a.Strength < a.Prefab.TreatmentThreshold); + targetCharacter.CharacterHealth.GetAllAfflictions().All(a => a.Strength <= a.Prefab.TreatmentThreshold); if (isCompleted && targetCharacter != character && character.IsOnPlayerTeam) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs index 73115b133..48936b9f6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs @@ -83,7 +83,7 @@ namespace Barotrauma if (character.AIController is HumanAIController humanAI) { if (GetVitalityFactor(target) >= GetVitalityThreshold(humanAI.ObjectiveManager, character, target) || - target.CharacterHealth.GetAllAfflictions().All(a => a.Strength < a.Prefab.TreatmentThreshold)) + target.CharacterHealth.GetAllAfflictions().All(a => a.Strength <= a.Prefab.TreatmentThreshold)) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs index e9e52e8c1..86756d255 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveReturn.cs @@ -12,7 +12,7 @@ namespace Barotrauma private bool usingEscapeBehavior; public Submarine ReturnTarget { get; } - public AIObjectiveReturn(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1.0f) : base(character, objectiveManager, priorityModifier) + public AIObjectiveReturn(Character character, Character orderGiver, AIObjectiveManager objectiveManager, float priorityModifier = 1.0f) : base(character, objectiveManager, priorityModifier) { ReturnTarget = GetReturnTarget(Submarine.MainSubs) ?? GetReturnTarget(Submarine.Loaded); if (ReturnTarget == null) @@ -23,10 +23,12 @@ namespace Barotrauma Submarine GetReturnTarget(IEnumerable subs) { + var requiredTeamID = orderGiver?.TeamID ?? character?.TeamID; Submarine returnTarget = null; foreach (var sub in subs) { - if (sub?.TeamID != character.TeamID) { continue; } + if (sub == null) { continue; } + if (sub.TeamID != requiredTeamID) { continue; } returnTarget = sub; break; } @@ -229,7 +231,7 @@ namespace Barotrauma protected override void OnAbandon() { base.OnAbandon(); - SteeringManager.Reset(); + SteeringManager?.Reset(); if (character.IsOnPlayerTeam && objectiveManager.CurrentOrder == objectiveManager.CurrentObjective) { string msg = TextManager.Get("dialogcannotreturn", returnNull: true); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs index 8ccdff754..c6cf25146 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs @@ -24,8 +24,6 @@ namespace Barotrauma public readonly Vector2 Position; public readonly int WayPointID; - public bool blocked; - public override string ToString() { return $"PathNode {WayPointID}"; @@ -81,6 +79,31 @@ namespace Barotrauma return nodeList; } + + private bool? blocked; + public bool IsBlocked() + { + if (blocked.HasValue) { return blocked.Value; } + + blocked = false; + + if (Waypoint.Submarine != null) { return blocked.Value; } + if (Waypoint.Tunnel?.Type != Level.TunnelType.Cave) { return blocked.Value; } + foreach (var w in Level.Loaded.ExtraWalls) + { + if (!(w is DestructibleLevelWall d)) { return blocked.Value; } + if (d.Destroyed) { return blocked.Value; } + if (!d.IsPointInside(Waypoint.Position)) { return blocked.Value; } + blocked = true; + break; + } + return blocked.Value; + } + + public void ResetBlocked() + { + blocked = null; + } } class PathFinder @@ -146,7 +169,10 @@ namespace Barotrauma 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) { - UpdateBlockedNodes(); + foreach (PathNode node in nodes) + { + node.ResetBlocked(); + } //sort nodes roughly according to distance sortedNodes.Clear(); @@ -202,11 +228,11 @@ namespace Barotrauma { if (startNode == null || node.TempDistance < startNode.TempDistance) { - if (node.blocked) { continue; } if (nodeFilter != null && !nodeFilter(node)) { continue; } if (startNodeFilter != null && !startNodeFilter(node)) { continue; } // Always check the visibility for the start node if (!IsWaypointVisible(node, start)) { continue; } + if (node.IsBlocked()) { continue; } startNode = node; } } @@ -251,11 +277,11 @@ namespace Barotrauma { if (endNode == null || node.TempDistance < endNode.TempDistance) { - if (node.blocked) { continue; } if (nodeFilter != null && !nodeFilter(node)) { continue; } if (endNodeFilter != null && !endNodeFilter(node)) { continue; } // Only check the visibility for the end node when allowed (fix leaks) if (!IsWaypointVisible(node, end, checkVisibility: checkVisibility)) { continue; } + if (node.IsBlocked()) { continue; } endNode = node; } } @@ -326,15 +352,13 @@ namespace Barotrauma float dist = float.MaxValue; foreach (PathNode node in nodes) { - if (node.state != 1) { continue; } + if (node.state != 1 || node.F > dist) { continue; } if (isCharacter && node.Waypoint.isObstructed) { continue; } - if (node.blocked) { continue; } if (filter != null && !filter(node)) { continue; } - if (node.F < dist) - { - dist = node.F; - currNode = node; - } + if (node.IsBlocked()) { continue; } + + dist = node.F; + currNode = node; } if (currNode == null || currNode == end) { break; } @@ -436,25 +460,6 @@ namespace Barotrauma return path; } - - private void UpdateBlockedNodes() - { - if (!isCharacter) { return; } - foreach (var n in nodes) - { - n.blocked = false; - if (n.Waypoint.Submarine != null) { continue; } - if (n.Waypoint.Tunnel?.Type != Level.TunnelType.Cave) { continue; } - foreach (var w in Level.Loaded.ExtraWalls) - { - if (!(w is DestructibleLevelWall d)) { continue; } - if (d.Destroyed) { continue; } - if (!d.IsPointInside(n.Waypoint.Position)) { continue; } - n.blocked = true; - break; - } - } - } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs index 8d490a3fa..f03fabde1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs @@ -42,6 +42,10 @@ namespace Barotrauma } else { + if (this is HumanoidAnimController humanAnimController && humanAnimController.Crouching) + { + return humanAnimController.HumanCrouchParams; + } return IsMovingFast ? RunParams : WalkParams; } } @@ -96,7 +100,12 @@ namespace Barotrauma { if (CanWalk) { - return new List { WalkParams, RunParams, SwimSlowParams, SwimFastParams }; + var anims = new List { WalkParams, RunParams, SwimSlowParams, SwimFastParams }; + if (this is HumanoidAnimController humanAnimController) + { + anims.Add(humanAnimController.HumanCrouchParams); + } + return anims; } else { @@ -154,7 +163,7 @@ namespace Barotrauma public virtual void UpdateUseItem(bool allowMovement, Vector2 handWorldPos) { } - public float GetSpeed(AnimationType type) + public virtual float GetSpeed(AnimationType type) { GroundedMovementParams movementParams; switch (type) @@ -207,7 +216,14 @@ namespace Barotrauma } else { - animType = AnimationType.Walk; + if (this is HumanoidAnimController humanAnimController && humanAnimController.Crouching) + { + animType = AnimationType.Crouch; + } + else + { + animType = AnimationType.Walk; + } } } return GetSpeed(animType); @@ -221,6 +237,12 @@ namespace Barotrauma return WalkParams; case AnimationType.Run: return RunParams; + case AnimationType.Crouch: + if (this is HumanoidAnimController humanAnimController) + { + return humanAnimController.HumanCrouchParams; + } + throw new NotImplementedException(type.ToString()); case AnimationType.SwimSlow: return SwimSlowParams; case AnimationType.SwimFast: diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs index a057b932d..8edee5073 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs @@ -97,9 +97,9 @@ namespace Barotrauma public new FishSwimParams CurrentSwimParams => base.CurrentSwimParams as FishSwimParams; public float? TailAngle => GetValidOrNull(CurrentAnimationParams, CurrentFishAnimation?.TailAngleInRadians); - public float FootTorque => CurrentFishAnimation.FootTorque; - public float HeadTorque => CurrentFishAnimation.HeadTorque; - public float TorsoTorque => CurrentFishAnimation.TorsoTorque; + public float FootTorque => CurrentAnimationParams.FootTorque; + public float HeadTorque => CurrentAnimationParams.HeadTorque; + public float TorsoTorque => CurrentAnimationParams.TorsoTorque; public float TailTorque => CurrentFishAnimation.TailTorque; public float HeadMoveForce => CurrentGroundedParams.HeadMoveForce; public float TorsoMoveForce => CurrentGroundedParams.TorsoMoveForce; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index 5032d2a2e..722cc1106 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -2,7 +2,6 @@ using FarseerPhysics; using Microsoft.Xna.Framework; using System; -using System.Collections.Generic; using System.Linq; using Barotrauma.Extensions; using Barotrauma.Networking; @@ -73,6 +72,20 @@ namespace Barotrauma set { _humanRunParams = value; } } + private HumanCrouchParams _humanCrouchParams; + public HumanCrouchParams HumanCrouchParams + { + get + { + if (_humanCrouchParams == null) + { + _humanCrouchParams = HumanCrouchParams.GetDefaultAnimParams(character); + } + return _humanCrouchParams; + } + set { _humanCrouchParams = value; } + } + private HumanSwimSlowParams _humanSwimSlowParams; public HumanSwimSlowParams HumanSwimSlowParams { @@ -102,8 +115,11 @@ namespace Barotrauma } public new HumanGroundedParams CurrentGroundedParams => base.CurrentGroundedParams as HumanGroundedParams; + public new HumanSwimParams CurrentSwimParams => base.CurrentSwimParams as HumanSwimParams; + public IHumanAnimation CurrentHumanAnimParams => CurrentAnimationParams as IHumanAnimation; + public override GroundedMovementParams WalkParams { get { return HumanWalkParams; } @@ -169,42 +185,9 @@ namespace Barotrauma private float swimmingStateLockTimer; private float useItemTimer; - - public override float? TorsoPosition - { - get - { - return Crouching && !swimming ? CurrentGroundedParams.CrouchingTorsoPos * RagdollParams.JointScale : base.TorsoPosition; - } - } - - public override float? HeadPosition - { - get - { - return Crouching && !swimming ? CurrentGroundedParams.CrouchingHeadPos * RagdollParams.JointScale : base.HeadPosition; - } - } - - public override float? TorsoAngle - { - get - { - return Crouching && !swimming ? MathHelper.ToRadians(CurrentGroundedParams.CrouchingTorsoAngle) : base.TorsoAngle; - } - } - - public override float? HeadAngle - { - get - { - return Crouching && !swimming ? MathHelper.ToRadians(CurrentGroundedParams.CrouchingHeadAngle) : base.HeadAngle; - } - } - public float HeadLeanAmount => CurrentGroundedParams.HeadLeanAmount; public float TorsoLeanAmount => CurrentGroundedParams.TorsoLeanAmount; - public Vector2 FootMoveOffset => (Crouching ? CurrentGroundedParams.CrouchingFootMoveOffset : CurrentGroundedParams.FootMoveOffset) * RagdollParams.JointScale; + public Vector2 FootMoveOffset => CurrentGroundedParams.FootMoveOffset * RagdollParams.JointScale; public float LegBendTorque => CurrentGroundedParams.LegBendTorque * RagdollParams.JointScale; public Vector2 HandMoveOffset => CurrentGroundedParams.HandMoveOffset * RagdollParams.JointScale; @@ -298,7 +281,7 @@ namespace Barotrauma LimbType lowerLegType = LimbType.RightLeg; LimbType footType = LimbType.RightFoot; - var waistJoint = GetJointBetweenLimbs(LimbType.Waist, upperLegType); + var waistJoint = GetJointBetweenLimbs(LimbType.Waist, upperLegType) ?? GetJointBetweenLimbs(LimbType.Torso, upperLegType); Vector2 localAnchorWaist = Vector2.Zero; Vector2 localAnchorKnee = Vector2.Zero; if (waistJoint != null) @@ -336,7 +319,7 @@ namespace Barotrauma levitatingCollider = true; ColliderIndex = Crouching && !swimming ? 1 : 0; if (character.SelectedConstruction?.GetComponent()?.ControlCharacterPose ?? false || - (ForceSelectAnimationType != AnimationType.Walk && ForceSelectAnimationType != AnimationType.NotDefined)) + (ForceSelectAnimationType != AnimationType.Crouch && ForceSelectAnimationType != AnimationType.NotDefined)) { Crouching = false; ColliderIndex = 0; @@ -439,9 +422,8 @@ 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); - HandIK(leftHand, midPos); + HandIK(rightHand, midPos, CurrentHumanAnimParams.ArmIKStrength, CurrentHumanAnimParams.HandIKStrength); + HandIK(leftHand, midPos, CurrentHumanAnimParams.ArmIKStrength, CurrentHumanAnimParams.HandIKStrength); } else if (character.AnimController.AnimationTestPose) { @@ -638,7 +620,7 @@ namespace Barotrauma Collider.LinearVelocity.Y > 0.0f ? Collider.LinearVelocity.Y * 0.5f : Collider.LinearVelocity.Y); } - getUpForce = getUpForce * Math.Max(head.SimPosition.Y - colliderPos.Y, 0.5f); + getUpForce *= Math.Max(head.SimPosition.Y - colliderPos.Y, 0.5f); torso.PullJointEnabled = true; head.PullJointEnabled = true; @@ -710,9 +692,12 @@ namespace Barotrauma float torsoAngle = TorsoAngle.Value; float herpesStrength = character.CharacterHealth.GetAfflictionStrength("spaceherpes"); torsoAngle -= herpesStrength / 150.0f; - torso.body.SmoothRotate(torsoAngle * Dir, 50.0f); + torso.body.SmoothRotate(torsoAngle * Dir, CurrentGroundedParams.TorsoTorque); + } + if (HeadAngle.HasValue) + { + head.body.SmoothRotate(HeadAngle.Value * Dir, CurrentGroundedParams.HeadTorque); } - if (HeadAngle.HasValue) head.body.SmoothRotate(HeadAngle.Value * Dir, 50.0f); if (!onGround) { @@ -743,12 +728,23 @@ namespace Barotrauma Vector2 footPos = stepSize * -i; footPos += new Vector2(Math.Sign(movement.X) * FootMoveOffset.X, FootMoveOffset.Y); - if (footPos.Y < 0.0f) footPos.Y = -0.15f; + if (footPos.Y < 0.0f) { footPos.Y = -0.15f; } //make the character limp if the feet are damaged float footAfflictionStrength = character.CharacterHealth.GetAfflictionStrength("damage", foot, true); footPos.X *= MathHelper.Lerp(1.0f, 0.75f, MathHelper.Clamp(footAfflictionStrength / 50.0f, 0.0f, 1.0f)); + if (CurrentGroundedParams.FootLiftHorizontalFactor > 0) + { + // Calculate the foot y dynamically based on the foot position relative to the waist, + // so that the foot aims higher when it's behind the waist and lower when it's in the front. + float xDiff = (foot.SimPosition.X - waistPos.X + FootMoveOffset.X) * Dir; + float min = MathUtils.InverseLerp(1, 0, CurrentGroundedParams.FootLiftHorizontalFactor); + float max = 1 + MathUtils.InverseLerp(0, 1, CurrentGroundedParams.FootLiftHorizontalFactor); + float xFactor = MathHelper.Lerp(min, max, MathUtils.InverseLerp(RagdollParams.JointScale, -RagdollParams.JointScale, xDiff)); + footPos.Y *= xFactor; + } + if (onSlope && Stairs == null) { footPos.Y *= 2.0f; @@ -770,7 +766,7 @@ namespace Barotrauma foot.DebugTargetPos = colliderPos + footPos; MoveLimb(foot, colliderPos + footPos, CurrentGroundedParams.FootMoveStrength); FootIK(foot, colliderPos + footPos, - CurrentGroundedParams.LegBendTorque, CurrentGroundedParams.FootRotateStrength, CurrentGroundedParams.FootAngleInRadians); + CurrentGroundedParams.LegBendTorque, CurrentGroundedParams.FootTorque, CurrentGroundedParams.FootAngleInRadians); } } @@ -789,7 +785,7 @@ namespace Barotrauma HandIK(rightHand, torso.SimPosition + posAddition + new Vector2( -handPos.X, - (Math.Sign(walkPosX) == Math.Sign(Dir)) ? handPos.Y : lowerY), CurrentGroundedParams.HandMoveStrength); + (Math.Sign(walkPosX) == Math.Sign(Dir)) ? handPos.Y : lowerY), CurrentGroundedParams.ArmMoveStrength, CurrentGroundedParams.HandMoveStrength); } if (leftHand != null && !leftHand.Disabled) @@ -797,16 +793,14 @@ namespace Barotrauma HandIK(leftHand, torso.SimPosition + posAddition + new Vector2( handPos.X, - (Math.Sign(walkPosX) == Math.Sign(-Dir)) ? handPos.Y : lowerY), CurrentGroundedParams.HandMoveStrength); + (Math.Sign(walkPosX) == Math.Sign(-Dir)) ? handPos.Y : lowerY), CurrentGroundedParams.ArmMoveStrength, CurrentGroundedParams.HandMoveStrength); } - } else { for (int i = -1; i < 2; i += 2) { Vector2 footPos = colliderPos; - if (Crouching) { footPos = new Vector2( @@ -817,27 +811,24 @@ namespace Barotrauma //lift the foot at the back up a bit footPos.Y += 0.15f; } - footPos.X += torso.SimPosition.X; + footPos.X += colliderPos.X; } else { footPos = new Vector2(colliderPos.X + stepSize.X * i * 0.2f, colliderPos.Y - 0.1f); } - if (Stairs == null) { footPos.Y = Math.Max(Math.Min(FloorY, footPos.Y + 0.5f), footPos.Y); } - var foot = i == -1 ? rightFoot : leftFoot; - if (foot != null && !foot.Disabled) { foot.DebugRefPos = colliderPos; foot.DebugTargetPos = footPos; MoveLimb(foot, footPos, CurrentGroundedParams.FootMoveStrength); FootIK(foot, footPos, - CurrentGroundedParams.LegBendTorque, CurrentGroundedParams.FootRotateStrength, CurrentGroundedParams.FootAngleInRadians); + CurrentGroundedParams.LegBendTorque, CurrentGroundedParams.FootTorque, CurrentGroundedParams.FootAngleInRadians); } } @@ -970,7 +961,7 @@ namespace Barotrauma if (!aiming) { float newRotation = MathUtils.VectorToAngle(TargetMovement) - MathHelper.PiOver2; - Collider.SmoothRotate(newRotation, 5.0f * character.SpeedMultiplier); + Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier); } } else @@ -981,7 +972,7 @@ namespace Barotrauma Vector2 diff = (mousePos - torso.SimPosition) * Dir; TargetMovement = new Vector2(0.0f, -0.1f); float newRotation = MathUtils.VectorToAngle(diff); - Collider.SmoothRotate(newRotation, 5.0f * character.SpeedMultiplier); + Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier); } } @@ -991,19 +982,19 @@ namespace Barotrauma if (TorsoAngle.HasValue) { - torso.body.SmoothRotate(Collider.Rotation + TorsoAngle.Value * Dir, CurrentSwimParams.SteerTorque); + torso.body.SmoothRotate(Collider.Rotation + TorsoAngle.Value * Dir, CurrentSwimParams.TorsoTorque); } else { - torso.body.SmoothRotate(Collider.Rotation, CurrentSwimParams.SteerTorque); + torso.body.SmoothRotate(Collider.Rotation, CurrentSwimParams.TorsoTorque); } if (HeadAngle.HasValue) { - head.body.SmoothRotate(Collider.Rotation + HeadAngle.Value * Dir, CurrentSwimParams.SteerTorque); + head.body.SmoothRotate(Collider.Rotation + HeadAngle.Value * Dir, CurrentSwimParams.HeadTorque); } else { - head.body.SmoothRotate(Collider.Rotation, CurrentSwimParams.SteerTorque); + head.body.SmoothRotate(Collider.Rotation, CurrentSwimParams.HeadTorque); } //dont try to move upwards if head is already out of water @@ -1044,25 +1035,25 @@ namespace Barotrauma float legMoveMultiplier = 1.0f; if (movement.LengthSquared() < 0.001f) { - //TODO: expose these? + // Swimming in place (TODO: expose?) legMoveMultiplier = 0.3f; legCyclePos += 0.4f; handCyclePos += 0.1f; } - var waist = GetLimb(LimbType.Waist); + var waist = GetLimb(LimbType.Waist) ?? GetLimb(LimbType.Torso); footPos = waist == null ? Vector2.Zero : waist.SimPosition - new Vector2((float)Math.Sin(-Collider.Rotation), (float)Math.Cos(-Collider.Rotation)) * (upperLegLength + lowerLegLength); Vector2 transformedFootPos = new Vector2((float)Math.Sin(legCyclePos / CurrentSwimParams.LegCycleLength) * CurrentSwimParams.LegMoveAmount * legMoveMultiplier, 0.0f); transformedFootPos = Vector2.Transform(transformedFootPos, Matrix.CreateRotationZ(Collider.Rotation)); - float torque = CurrentSwimParams.FootRotateStrength * character.SpeedMultiplier * (1.2f - character.GetLegPenalty()); + float legTorque = CurrentSwimParams.LegTorque * character.SpeedMultiplier * (1.2f - character.GetLegPenalty()); if (rightFoot != null && !rightFoot.Disabled) { - FootIK(rightFoot, footPos - transformedFootPos, torque, torque, CurrentSwimParams.FootAngleInRadians); + FootIK(rightFoot, footPos - transformedFootPos, legTorque, CurrentSwimParams.FootTorque, CurrentSwimParams.FootAngleInRadians); } if (leftFoot != null && !leftFoot.Disabled) { - FootIK(leftFoot, footPos + transformedFootPos, torque, torque, CurrentSwimParams.FootAngleInRadians); + FootIK(leftFoot, footPos + transformedFootPos, legTorque, CurrentSwimParams.FootTorque, CurrentSwimParams.FootAngleInRadians); } handPos = (torso.SimPosition + head.SimPosition) / 2.0f; @@ -1071,7 +1062,7 @@ namespace Barotrauma // -> hands just float around if ((!headInWater && TargetMovement.X == 0.0f && TargetMovement.Y > 0) || TargetMovement.LengthSquared() < 0.001f) { - handPos += MathUtils.RotatePoint(Vector2.UnitX * Dir * 0.6f, torso.Rotation); + handPos += MathUtils.RotatePoint(Vector2.UnitX * Dir * 0.2f, torso.Rotation); float wobbleAmount = 0.1f; @@ -1079,14 +1070,14 @@ namespace Barotrauma { MoveLimb(rightHand, new Vector2( handPos.X + (float)Math.Sin(handCyclePos / 1.5f) * wobbleAmount, - handPos.Y + (float)Math.Sin(handCyclePos / 3.5f) * wobbleAmount - 0.25f), 1.5f); + handPos.Y + (float)Math.Sin(handCyclePos / 3.5f) * wobbleAmount - 0.25f), CurrentSwimParams.ArmMoveStrength); } if (leftHand != null && !leftHand.Disabled) { MoveLimb(leftHand, new Vector2( handPos.X + (float)Math.Sin(handCyclePos / 2.0f) * wobbleAmount, - handPos.Y + (float)Math.Sin(handCyclePos / 3.0f) * wobbleAmount - 0.25f), 1.5f); + handPos.Y + (float)Math.Sin(handCyclePos / 3.0f) * wobbleAmount - 0.25f), CurrentSwimParams.ArmMoveStrength); } return; @@ -1107,8 +1098,8 @@ namespace Barotrauma Vector2 rightHandPos = new Vector2(-handPosX, -handPosY) + handMoveOffset; rightHandPos.X = (Dir == 1.0f) ? Math.Max(0.3f, rightHandPos.X) : Math.Min(-0.3f, rightHandPos.X); rightHandPos = Vector2.Transform(rightHandPos, rotationMatrix); - - HandIK(rightHand, handPos + rightHandPos, CurrentSwimParams.HandMoveStrength * character.SpeedMultiplier * (1 - Character.GetRightHandPenalty())); + float speedMultiplier = character.SpeedMultiplier * (1 - Character.GetRightHandPenalty()); + HandIK(rightHand, handPos + rightHandPos, CurrentSwimParams.ArmMoveStrength * speedMultiplier, CurrentSwimParams.HandMoveStrength * speedMultiplier); } if (leftHand != null && !leftHand.Disabled) @@ -1116,8 +1107,8 @@ namespace Barotrauma Vector2 leftHandPos = new Vector2(handPosX, handPosY) + handMoveOffset; leftHandPos.X = (Dir == 1.0f) ? Math.Max(0.3f, leftHandPos.X) : Math.Min(-0.3f, leftHandPos.X); leftHandPos = Vector2.Transform(leftHandPos, rotationMatrix); - - HandIK(leftHand, handPos + leftHandPos, CurrentSwimParams.HandMoveStrength * character.SpeedMultiplier * (1 - Character.GetLeftHandPenalty())); + float speedMultiplier = character.SpeedMultiplier * (1 - Character.GetLeftHandPenalty()); + HandIK(leftHand, handPos + leftHandPos, CurrentSwimParams.ArmMoveStrength * speedMultiplier, CurrentSwimParams.HandMoveStrength * speedMultiplier); } } @@ -1173,15 +1164,16 @@ namespace Barotrauma } float bottomPos = Collider.SimPosition.Y - ColliderHeightFromFloor - Collider.radius - Collider.height / 2.0f; + float headPos = HeadPosition ?? 0; + float torsoPos = TorsoPosition ?? 0; + MoveLimb(head, new Vector2(ladderSimPos.X - 0.2f * Dir, bottomPos + headPos), 10.5f); + MoveLimb(torso, new Vector2(ladderSimPos.X - 0.35f * Dir, bottomPos + torsoPos), 10.5f); - MoveLimb(head, new Vector2(ladderSimPos.X - 0.35f * Dir, bottomPos + WalkParams.HeadPosition), 10.5f); - MoveLimb(torso, new Vector2(ladderSimPos.X - 0.35f * Dir, bottomPos + WalkParams.TorsoPosition), 10.5f); - - Collider.MoveToPos(new Vector2(ladderSimPos.X - 0.1f * Dir, Collider.SimPosition.Y), 10.5f); + Collider.MoveToPos(new Vector2(ladderSimPos.X - 0.1f * Dir, Collider.SimPosition.Y), 10.5f); Vector2 handPos = new Vector2( ladderSimPos.X, - bottomPos + WalkParams.TorsoPosition + movement.Y * 0.1f - ladderSimPos.Y); + bottomPos + torsoPos + movement.Y * 0.1f - ladderSimPos.Y); //prevent the hands from going above the top of the ladders handPos.Y = Math.Min(-0.5f, handPos.Y); @@ -1258,7 +1250,8 @@ namespace Barotrauma //apply forces to the collider to move the Character up/down Collider.ApplyForce((climbForce * 20.0f + subSpeed * 50.0f) * Collider.Mass, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); - head.body.SmoothRotate(0.0f); + float movementMultiplier = targetMovement.Y < 0 ? 0 : 1; + head.body.SmoothRotate(MathHelper.PiOver4 * movementMultiplier * Dir, WalkParams.HeadTorque); if (!character.SelectedConstruction.Prefab.Triggers.Any()) { @@ -1881,7 +1874,7 @@ namespace Barotrauma 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]); + HandIK(i == 0 ? rightHand : leftHand, transformedHoldPos + transformedHandlePos[i], CurrentHumanAnimParams.ArmIKStrength, CurrentHumanAnimParams.HandIKStrength); } } } @@ -1906,7 +1899,7 @@ namespace Barotrauma return (lowFreqNoise * 1.0f + highFreqNoise * 0.1f) * wobbleStrength; } - private void HandIK(Limb hand, Vector2 pos, float force = 1.0f) + private void HandIK(Limb hand, Vector2 pos, float armTorque = 1.0f, float handTorque = 1.0f) { Vector2 shoulderPos; @@ -1948,9 +1941,11 @@ namespace Barotrauma armAngle -= MathHelper.TwoPi; } - arm?.body.SmoothRotate((armAngle - upperArmAngle), 20.0f * force * arm.Mass, wrapAngle: false); - forearm?.body.SmoothRotate((armAngle + lowerArmAngle), 20.0f * force * forearm.Mass, wrapAngle: false); - hand?.body.SmoothRotate((armAngle + lowerArmAngle), 100.0f * force * hand.Mass, wrapAngle: false); + 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) @@ -1976,12 +1971,12 @@ namespace Barotrauma upperLeg = GetLimb(LimbType.RightThigh); lowerLeg = GetLimb(LimbType.RightLeg); } - var torso = GetLimb(LimbType.Torso); - var waist = GetJointBetweenLimbs(LimbType.Waist, upperLeg.type); + Limb torso = GetLimb(LimbType.Torso); + LimbJoint waistJoint = GetJointBetweenLimbs(LimbType.Waist, upperLeg.type) ?? GetJointBetweenLimbs(LimbType.Torso, upperLeg.type); Vector2 waistPos = Vector2.Zero; - if (waist != null) + if (waistJoint != null) { - waistPos = waist.LimbA == upperLeg ? waist.WorldAnchorA : waist.WorldAnchorB; + waistPos = waistJoint.LimbA == upperLeg ? waistJoint.WorldAnchorA : waistJoint.WorldAnchorB; } //distance from waist joint to the target position @@ -2137,5 +2132,18 @@ namespace Barotrauma } } + public override float GetSpeed(AnimationType type) + { + if (type == AnimationType.Crouch) + { + if (!CanWalk) + { + DebugConsole.ThrowError($"{character.SpeciesName} cannot crouch!"); + return 0; + } + return IsMovingBackwards ? HumanCrouchParams.MovementSpeed * HumanCrouchParams.BackwardsMovementMultiplier : HumanCrouchParams.MovementSpeed; + } + return base.GetSpeed(type); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index da6c1677b..2d748476e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -309,8 +309,6 @@ namespace Barotrauma public string TraitorCurrentObjective = ""; public bool IsHuman => SpeciesName.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase); - public bool IsMale => Info != null && Info.HasGenders && Info.Gender == Gender.Male; - public bool IsFemale => Info != null && Info.HasGenders && Info.Gender == Gender.Female; private float attackCoolDown; @@ -1665,9 +1663,9 @@ namespace Barotrauma AnimController.IgnorePlatforms = AnimController.TargetMovement.Y < -0.1f; } - if (AnimController is HumanoidAnimController) + if (AnimController is HumanoidAnimController humanAnimController) { - ((HumanoidAnimController)AnimController).Crouching = IsKeyDown(InputType.Crouch); + humanAnimController.Crouching = humanAnimController.ForceSelectAnimationType == AnimationType.Crouch || IsKeyDown(InputType.Crouch); } if (!aiControlled && @@ -2760,9 +2758,9 @@ namespace Barotrauma //ragdoll button if (IsRagdolled || !CanMove) { - if (AnimController is HumanoidAnimController) + if (AnimController is HumanoidAnimController humanAnimController) { - ((HumanoidAnimController)AnimController).Crouching = false; + humanAnimController.Crouching = false; } AnimController.ResetPullJoints(); SelectedConstruction = null; @@ -3505,9 +3503,9 @@ namespace Barotrauma } } - public AttackResult AddDamage(Vector2 worldPosition, IEnumerable afflictions, float stun, bool playSound, float attackImpulse = 0.0f, Character attacker = null) + public AttackResult AddDamage(Vector2 worldPosition, IEnumerable afflictions, float stun, bool playSound, float attackImpulse = 0.0f, Character attacker = null, float damageMultiplier = 1f) { - return AddDamage(worldPosition, afflictions, stun, playSound, attackImpulse, out _, attacker); + return AddDamage(worldPosition, afflictions, stun, playSound, attackImpulse, out _, attacker, damageMultiplier: damageMultiplier); } public AttackResult AddDamage(Vector2 worldPosition, IEnumerable afflictions, float stun, bool playSound, float attackImpulse, out Limb hitLimb, Character attacker = null, float damageMultiplier = 1) @@ -4351,7 +4349,7 @@ namespace Barotrauma return GiveTalent(talentPrefab, addingFirstTime); } - private bool GiveTalent(TalentPrefab talentPrefab, bool addingFirstTime = true) + public bool GiveTalent(TalentPrefab talentPrefab, bool addingFirstTime = true) { if (addingFirstTime) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index e4190f0ea..1627fac16 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -1,9 +1,9 @@ using Barotrauma.Extensions; using Barotrauma.Items.Components; -using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using Barotrauma.IO; using System.Linq; using System.Xml.Linq; @@ -14,7 +14,6 @@ namespace Barotrauma public enum Gender { None, Male, Female }; public enum Race { None, White, Black, Brown, Asian }; - // TODO: Generating the HeadInfo could be simplified. partial class CharacterInfo { public class HeadInfo @@ -25,15 +24,7 @@ namespace Barotrauma get { return _headSpriteId; } set { - _headSpriteId = value; - if (_headSpriteId < (int)headSpriteRange.X) - { - _headSpriteId = (int)headSpriteRange.Y; - } - if (_headSpriteId > (int)headSpriteRange.Y) - { - _headSpriteId = (int)headSpriteRange.X; - } + _headSpriteId = Math.Max(Math.Clamp(value, (int)headSpriteRange.X, (int)headSpriteRange.Y), 1); GetSpriteSheetIndex(); } } @@ -42,6 +33,10 @@ namespace Barotrauma public Gender gender; public Race race; + public Color HairColor; + public Color FacialHairColor; + public Color SkinColor; + public int HairIndex { get; set; } = -1; public int BeardIndex { get; set; } = -1; public int MoustacheIndex { get; set; } = -1; @@ -74,11 +69,11 @@ namespace Barotrauma FaceAttachmentIndex = -1; } - private void GetSpriteSheetIndex() + public void GetSpriteSheetIndex() { if (heads != null && heads.Any()) { - var matchingHead = heads.Keys.FirstOrDefault(h => h.Gender == gender && h.Race == race && h.ID == _headSpriteId); + var matchingHead = heads.Keys.FirstOrDefault(h => h.ID == HeadSpriteId && IsMatchingGender(h.Gender, gender) && IsMatchingRace(h.Race, race)); if (matchingHead != null) { if (heads.TryGetValue(matchingHead, out Vector2 index)) @@ -99,14 +94,13 @@ namespace Barotrauma if (head != value && value != null) { head = value; - if (head.race == Race.None) + if (!IsValidRace(head.race)) { head.race = GetRandomRace(Rand.RandSync.Unsynced); } CalculateHeadSpriteRange(); Head.HeadSpriteId = value.HeadSpriteId; - HeadSprite = null; - AttachmentSprites = null; + RefreshHeadSprites(); } } } @@ -157,7 +151,9 @@ namespace Barotrauma public bool HasNickname => Name != OriginalName; public string OriginalName { get; private set; } + public string Name; + public string DisplayName { get @@ -387,29 +383,31 @@ namespace Barotrauma set { Head.HeadSpriteId = value; - HeadSprite = null; - AttachmentSprites = null; ResetHeadAttachments(); + RefreshHeadSprites(); } } public readonly bool HasGenders; + public readonly bool HasRaces; public Gender Gender { get { return Head.gender; } set { - if (Head.gender == value) return; + Gender previousValue = Head.gender; Head.gender = value; - if (Head.gender == Gender.None) + if (!IsValidGender(Head.gender)) { - Head.gender = Gender.Male; + Head.gender = GetDefaultGender(); + } + if (Head.gender != previousValue) + { + CalculateHeadSpriteRange(); + ResetHeadAttachments(); + RefreshHeadSprites(); } - CalculateHeadSpriteRange(); - ResetHeadAttachments(); - HeadSprite = null; - AttachmentSprites = null; } } @@ -418,28 +416,82 @@ namespace Barotrauma get { return Head.race; } set { - if (Head.race == value) { return; } + Race previousValue = Head.race; Head.race = value; - if (Head.race == Race.None) + if (!IsValidRace(Head.race)) { - Head.race = Race.White; + Head.race = GetDefaultRace(); + } + if (Head.race != previousValue) + { + CalculateHeadSpriteRange(); + ResetHeadAttachments(); + RefreshHeadSprites(); } - CalculateHeadSpriteRange(); - ResetHeadAttachments(); - HeadSprite = null; - AttachmentSprites = null; } } - public int HairIndex { get => Head.HairIndex; set => Head.HairIndex = value; } - public int BeardIndex { get => Head.BeardIndex; set => Head.BeardIndex = value; } - public int MoustacheIndex { get => Head.MoustacheIndex; set => Head.MoustacheIndex = value; } - public int FaceAttachmentIndex { get => Head.FaceAttachmentIndex; set => Head.FaceAttachmentIndex = value; } + private bool IsValidRace(Race race) => HasRaces ? race != Race.None : race == Race.None; - public XElement HairElement { get => Head.HairElement; set => Head.HairElement = value; } - public XElement BeardElement { get => Head.BeardElement; set => Head.BeardElement = value; } - public XElement MoustacheElement { get => Head.MoustacheElement; set => Head.MoustacheElement = value; } - public XElement FaceAttachment { get => Head.FaceAttachment; set => Head.FaceAttachment = value; } + private bool IsValidGender(Gender gender) => HasGenders ? gender != Gender.None : gender == Gender.None; + + private Gender GetDefaultGender() => HasGenders ? Gender.Male : Gender.None; + + private Race GetDefaultRace() => HasRaces ? Race.White : Race.None; + + public int HairIndex + { + get => Head.HairIndex; + set => Head.HairIndex = value; + } + + public int BeardIndex + { + get => Head.BeardIndex; + set => Head.BeardIndex = value; + } + + public int MoustacheIndex + { + get => Head.MoustacheIndex; + set => Head.MoustacheIndex = value; + } + + public int FaceAttachmentIndex + { + get => Head.FaceAttachmentIndex; + set => Head.FaceAttachmentIndex = value; + } + + public readonly ImmutableArray HairColors; + public readonly ImmutableArray FacialHairColors; + public readonly ImmutableArray SkinColors; + + public Color HairColor + { + get => Head.HairColor; + set => Head.HairColor = value; + } + + public Color FacialHairColor + { + get => Head.FacialHairColor; + set => Head.FacialHairColor = value; + } + + public Color SkinColor + { + get => Head.SkinColor; + set => Head.SkinColor = value; + } + + public XElement HairElement => Head.HairElement; + + public XElement BeardElement => Head.BeardElement; + + public XElement MoustacheElement => Head.MoustacheElement; + + public XElement FaceAttachment => Head.FaceAttachment; private RagdollParams ragdoll; public RagdollParams Ragdoll @@ -480,16 +532,18 @@ namespace Barotrauma if (doc == null) { return; } CharacterConfigElement = doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root; // TODO: support for variants - head = new HeadInfo(); + Head = new HeadInfo(); HasGenders = CharacterConfigElement.GetAttributeBool("genders", false); - if (HasGenders) - { - Head.gender = GetRandomGender(randSync); - } + Head.gender = GetRandomGender(randSync); + HasRaces = CharacterConfigElement.GetAttributeBool("races", false); Head.race = GetRandomRace(randSync); CalculateHeadSpriteRange(); - Head.HeadSpriteId = GetRandomHeadID(randSync); + HeadSpriteId = GetRandomHeadID(randSync); Job = (jobPrefab == null) ? Job.Random(Rand.RandSync.Unsynced) : new Job(jobPrefab, variant); + HairColors = CharacterConfigElement.GetAttributeColorArray("haircolors", new Color[] { Color.WhiteSmoke }).ToImmutableArray(); + FacialHairColors = CharacterConfigElement.GetAttributeColorArray("facialhaircolors", new Color[] { Color.WhiteSmoke }).ToImmutableArray(); + SkinColors = CharacterConfigElement.GetAttributeColorArray("skincolors", new Color[] { new Color(255, 215, 200, 255) }).ToImmutableArray(); + SetColors(); if (!string.IsNullOrEmpty(name)) { @@ -502,23 +556,7 @@ namespace Barotrauma else { name = ""; - if (CharacterConfigElement.Element("name") != null) - { - string firstNamePath = CharacterConfigElement.Element("name").GetAttributeString("firstname", ""); - if (firstNamePath != "") - { - firstNamePath = firstNamePath.Replace("[GENDER]", (Head.gender == Gender.Female) ? "female" : "male"); - Name = ToolBox.GetRandomLine(firstNamePath, randSync); - } - - string lastNamePath = CharacterConfigElement.Element("name").GetAttributeString("lastname", ""); - if (lastNamePath != "") - { - lastNamePath = lastNamePath.Replace("[GENDER]", (Head.gender == Gender.Female) ? "female" : "male"); - if (Name != "") Name += " "; - Name += ToolBox.GetRandomLine(lastNamePath, randSync); - } - } + Name = GetRandomName(randSync); } OriginalName = !string.IsNullOrEmpty(originalName) ? originalName : Name; personalityTrait = NPCPersonalityTrait.GetRandom(name + HeadSpriteId); @@ -530,6 +568,53 @@ namespace Barotrauma LoadHeadAttachments(); } + public string GetRandomName(Rand.RandSync randSync) + { + string name = ""; + if (CharacterConfigElement.Element("name") != null) + { + string firstNamePath = CharacterConfigElement.Element("name").GetAttributeString("firstname", ""); + if (firstNamePath != "") + { + firstNamePath = firstNamePath.Replace("[GENDER]", (Head.gender == Gender.Female) ? "female" : "male"); + name = ToolBox.GetRandomLine(firstNamePath, randSync); + } + + string lastNamePath = CharacterConfigElement.Element("name").GetAttributeString("lastname", ""); + if (lastNamePath != "") + { + lastNamePath = lastNamePath.Replace("[GENDER]", (Head.gender == Gender.Female) ? "female" : "male"); + if (name != "") { name += " "; } + name += ToolBox.GetRandomLine(lastNamePath, randSync); + } + } + + return name; + } + + private void SetColors() + { + HairColor = HairColors.GetRandom(); + FacialHairColor = FacialHairColors.GetRandom(); + SkinColor = SkinColors.GetRandom(); + } + + private void CheckColors() + { + if (HairColor == Color.Black) + { + HairColor = HairColors.GetRandom(); + } + if (FacialHairColor == Color.Black) + { + FacialHairColor = FacialHairColors.GetRandom(); + } + if (SkinColor == Color.Black) + { + SkinColor = SkinColors.GetRandom(); + } + } + // Used for loading the data public CharacterInfo(XElement infoElement) { @@ -542,7 +627,7 @@ namespace Barotrauma ExperiencePoints = infoElement.GetAttributeInt("experiencepoints", 0); UnlockedTalents = new HashSet(infoElement.GetAttributeStringArray("unlockedtalents", new string[0], convertToLowerInvariant: true)); AdditionalTalentPoints = infoElement.GetAttributeInt("additionaltalentpoints", 0); - Enum.TryParse(infoElement.GetAttributeString("race", "White"), true, out Race race); + Enum.TryParse(infoElement.GetAttributeString("race", "None"), true, out Race race); Enum.TryParse(infoElement.GetAttributeString("gender", "None"), true, out Gender gender); _speciesName = infoElement.GetAttributeString("speciesname", null); XDocument doc = null; @@ -560,14 +645,19 @@ namespace Barotrauma // TODO: support for variants CharacterConfigElement = doc.Root.IsOverride() ? doc.Root.FirstElement() : doc.Root; HasGenders = CharacterConfigElement.GetAttributeBool("genders", false); - if (HasGenders && gender == Gender.None) + HasRaces = CharacterConfigElement.GetAttributeBool("hasraces", false); + if (!IsValidGender(gender)) { gender = GetRandomGender(Rand.RandSync.Unsynced); } - else if (!HasGenders) + if (!IsValidRace(race)) { - gender = Gender.None; + race = GetRandomRace(Rand.RandSync.Unsynced); } + HairColors = CharacterConfigElement.GetAttributeColorArray("haircolors", new Color[] { Color.WhiteSmoke }).ToImmutableArray(); + FacialHairColors = CharacterConfigElement.GetAttributeColorArray("facialhaircolors", new Color[] { Color.WhiteSmoke }).ToImmutableArray(); + SkinColors = CharacterConfigElement.GetAttributeColorArray("skincolors", new Color[] { new Color(255, 215, 200, 255) }).ToImmutableArray(); + RecreateHead( infoElement.GetAttributeInt("headspriteid", 1), race, @@ -577,6 +667,11 @@ namespace Barotrauma infoElement.GetAttributeInt("moustacheindex", -1), infoElement.GetAttributeInt("faceattachmentindex", -1)); + SkinColor = infoElement.GetAttributeColor("skincolor", Color.White); + HairColor = infoElement.GetAttributeColor("haircolor", Color.White); + FacialHairColor = infoElement.GetAttributeColor("facialhaircolor", Color.White); + CheckColors(); + if (string.IsNullOrEmpty(Name)) { if (CharacterConfigElement.Element("name") != null) @@ -652,8 +747,25 @@ namespace Barotrauma LoadHeadAttachments(); } - public Gender GetRandomGender(Rand.RandSync randSync) => (Rand.Range(0.0f, 1.0f, randSync) < CharacterConfigElement.GetAttributeFloat("femaleratio", 0.5f)) ? Gender.Female : Gender.Male; - public Race GetRandomRace(Rand.RandSync randSync) => new Race[] { Race.White, Race.Black, Race.Asian }.GetRandom(randSync); + public Gender GetRandomGender(Rand.RandSync randSync) + { + if (HasGenders) + { + return (Rand.Range(0.0f, 1.0f, randSync) < CharacterConfigElement.GetAttributeFloat("femaleratio", 0.5f)) ? Gender.Female : Gender.Male; + } + return Gender.None; + } + + public Race GetRandomRace(Rand.RandSync randSync) + { + if (HasRaces) + { + return new Race[] { Race.White, Race.Black, Race.Asian }.GetRandom(randSync); + } + return Race.None; + } + + public int GetRandomHeadID(Rand.RandSync randSync) => Head.headSpriteRange != Vector2.Zero ? Rand.Range((int)Head.headSpriteRange.X, (int)Head.headSpriteRange.Y + 1, randSync) : 0; private List hairs; @@ -720,10 +832,13 @@ namespace Barotrauma { if (elements == null) { return elements; } return elements.Where(w => - Enum.TryParse(w.GetAttributeString("gender", "None"), true, out Gender g) && g == gender && - Enum.TryParse(w.GetAttributeString("race", "None"), true, out Race r) && r == race); + IsMatchingGender(Enum.Parse(w.GetAttributeString("gender", "None"), ignoreCase: true), gender) && + IsMatchingRace(Enum.Parse(w.GetAttributeString("race", "None"), ignoreCase: true), race)); } + public static bool IsMatchingGender(Gender gender, Gender myGender) => gender == Gender.None || gender == myGender; + public static bool IsMatchingRace(Race race, Race myRace) => race == Race.None || race == myRace; + private void LoadHeadPresets() { if (CharacterConfigElement == null) { return; } @@ -752,9 +867,16 @@ namespace Barotrauma // If there are any head presets defined, use them. if (heads.Any()) { - var ids = heads.Keys.Where(h => h.Race == Race && h.Gender == Gender).Select(w => w.ID); + var ids = heads.Keys.Where(h => IsMatchingRace(Race, h.Race) && IsMatchingGender(Gender, h.Gender)).Select(w => w.ID); ids = ids.OrderBy(id => id); - Head.headSpriteRange = new Vector2(ids.First(), ids.Last()); + if (ids.Any()) + { + Head.headSpriteRange = new Vector2(ids.First(), ids.Last()); + } + else + { + DebugConsole.ThrowError($"[CharacterInfo] Couldn't find a head definition that matches {Race} and {Gender}!"); + } } // Else we calculate the range from the wearables. if (Head.headSpriteRange == Vector2.Zero) @@ -787,26 +909,72 @@ namespace Barotrauma } } + public void RecreateHead(HeadInfo headInfo) + { + RecreateHead( + headInfo.HeadSpriteId, + headInfo.race, + headInfo.gender, + headInfo.HairIndex, + headInfo.BeardIndex, + headInfo.MoustacheIndex, + headInfo.FaceAttachmentIndex); + + SkinColor = headInfo.SkinColor; + HairColor = headInfo.HairColor; + FacialHairColor = headInfo.FacialHairColor; + CheckColors(); + } + + /// + /// Recreates the head info and checks that everything is valid. + /// public void RecreateHead(int headID, Race race, Gender gender, int hairIndex, int beardIndex, int moustacheIndex, int faceAttachmentIndex) { - if (HasGenders && gender == Gender.None) + if (!IsValidGender(gender)) { gender = GetRandomGender(Rand.RandSync.Unsynced); } - else if (!HasGenders) + if (!IsValidRace(race)) { - gender = Gender.None; + race = GetRandomRace(Rand.RandSync.Unsynced); } if (heads == null) { LoadHeadPresets(); } - head = new HeadInfo(headID, gender, race, hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex); + Color skin = Color.Black; + Color hair = Color.Black; + Color facialHair = Color.Black; + if (head != null) + { + skin = head.SkinColor; + hair = head.HairColor; + facialHair = head.FacialHairColor; + } + head = new HeadInfo(headID, gender, race, hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex) + { + SkinColor = skin, + HairColor = hair, + FacialHairColor = facialHair + }; CalculateHeadSpriteRange(); ReloadHeadAttachments(); + RefreshHead(); } - public void LoadHeadSprite() + /// + /// Reloads the head sprite and the attachment sprites. + /// + public void RefreshHead() + { + ReloadHeadAttachments(); + RefreshHeadSprites(); + } + + partial void LoadHeadSpriteProjectSpecific(XElement limbElement); + + private void LoadHeadSprite() { foreach (XElement limbElement in Ragdoll.MainElement.Elements()) { @@ -816,6 +984,7 @@ namespace Barotrauma if (spriteElement == null) { continue; } string spritePath = spriteElement.Attribute("texture").Value; + if (string.IsNullOrEmpty(spritePath)) { continue; } spritePath = spritePath.Replace("[GENDER]", (Head.gender == Gender.Female) ? "female" : "male"); spritePath = spritePath.Replace("[RACE]", Head.race.ToString().ToLowerInvariant()); @@ -823,6 +992,8 @@ namespace Barotrauma string fileName = Path.GetFileNameWithoutExtension(spritePath); + if (string.IsNullOrEmpty(fileName)) { continue; } + //go through the files in the directory to find a matching sprite foreach (string file in Directory.GetFiles(Path.GetDirectoryName(spritePath))) { @@ -847,13 +1018,12 @@ namespace Barotrauma break; } + LoadHeadSpriteProjectSpecific(limbElement); + break; } } - /// - /// Loads only the elements according to the indices, not the sprites. - /// public void LoadHeadAttachments() { if (Wearables != null) @@ -1138,7 +1308,7 @@ namespace Barotrauma new XAttribute("name", Name), new XAttribute("originalname", OriginalName), new XAttribute("speciesname", SpeciesName), - new XAttribute("gender", Head.gender == Gender.Male ? "male" : "female"), + new XAttribute("gender", Head.gender.ToString()), new XAttribute("race", Head.race.ToString()), new XAttribute("salary", Salary), new XAttribute("experiencepoints", ExperiencePoints), @@ -1149,6 +1319,9 @@ namespace Barotrauma new XAttribute("beardindex", BeardIndex), new XAttribute("moustacheindex", MoustacheIndex), new XAttribute("faceattachmentindex", FaceAttachmentIndex), + new XAttribute("skincolor", XMLExtensions.ColorToString(SkinColor)), + new XAttribute("haircolor", XMLExtensions.ColorToString(HairColor)), + new XAttribute("facialhaircolor", XMLExtensions.ColorToString(FacialHairColor)), new XAttribute("startitemsgiven", StartItemsGiven), new XAttribute("ragdoll", ragdollFileName), new XAttribute("personality", personalityTrait == null ? "" : personalityTrait.Name)); @@ -1462,13 +1635,19 @@ namespace Barotrauma if (healthData != null) { character?.CharacterHealth.Load(healthData); } } - public void ReloadHeadAttachments() + /// + /// Reloads the attachment xml elements according to the indices. Doesn't reload the sprites. + /// + private void ReloadHeadAttachments() { ResetLoadedAttachments(); LoadHeadAttachments(); } - public void ResetHeadAttachments() + /// + /// Loads only the elements according to the indices, not the sprites. + /// + private void ResetHeadAttachments() { ResetAttachmentIndices(); ResetLoadedAttachments(); @@ -1500,6 +1679,12 @@ namespace Barotrauma AttachmentSprites = null; } + private void RefreshHeadSprites() + { + HeadSprite = null; + AttachmentSprites = null; + } + // This could maybe be a LookUp instead? private readonly Dictionary> savedStatValues = new Dictionary>(); @@ -1541,7 +1726,7 @@ namespace Barotrauma } } - public void ChangeSavedStatValue(StatTypes statType, float value, string statIdentifier, bool removeOnDeath, bool removeAfterRound = false, float maxValue = float.MaxValue) + public void ChangeSavedStatValue(StatTypes statType, float value, string statIdentifier, bool removeOnDeath, bool removeAfterRound = false, float maxValue = float.MaxValue, bool setValue = false) { if (!savedStatValues.ContainsKey(statType)) { @@ -1550,7 +1735,7 @@ namespace Barotrauma if (savedStatValues[statType].FirstOrDefault(s => s.StatIdentifier == statIdentifier) is SavedStatValue savedStat) { - savedStat.StatValue = MathHelper.Min(savedStat.StatValue + value, maxValue); + savedStat.StatValue = setValue ? value : MathHelper.Min(savedStat.StatValue + value, maxValue); } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index 809ccf081..f227a11c0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -628,6 +628,11 @@ namespace Barotrauma Description = TextManager.Get("AfflictionDescription." + translationId, true) ?? element.GetAttributeString("description", ""); IsBuff = element.GetAttributeBool("isbuff", false); + if (element.Attribute("nameidentifier") != null) + { + Name = TextManager.Get(element.GetAttributeString("nameidentifier", string.Empty), returnNull: true) ?? Name; + } + LimbSpecific = element.GetAttributeBool("limbspecific", false); if (!LimbSpecific) { @@ -669,6 +674,15 @@ namespace Barotrauma case "afflictionoverlay": AfflictionOverlay = new Sprite(subElement); break; + case "statvalue": + DebugConsole.ThrowError($"Error in affliction \"{Identifier}\" - stat values should be configured inside the affliction's effects."); + break; + case "effect": + case "periodiceffect": + break; + default: + DebugConsole.AddWarning($"Unrecognized element in affliction \"{Identifier}\" ({subElement.Name})"); + break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index be9258701..83ff1bad4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -979,7 +979,7 @@ namespace Barotrauma float minSuitability = -10, maxSuitability = 10; foreach (Affliction affliction in getAfflictions(limb)) { - if (affliction.Strength < affliction.Prefab.TreatmentThreshold) { continue; } + if (affliction.Strength <= affliction.Prefab.TreatmentThreshold) { continue; } if (ignoreHiddenAfflictions && affliction.Strength < affliction.Prefab.ShowIconThreshold) { continue; } foreach (KeyValuePair treatment in affliction.Prefab.TreatmentSuitability) { @@ -1088,7 +1088,7 @@ namespace Barotrauma /// Automatically filters out buffs. /// public static IEnumerable SortAfflictionsBySeverity(IEnumerable afflictions, bool excludeBuffs = true) => - afflictions.Where(a => !excludeBuffs || !a.Prefab.IsBuff).OrderByDescending(a => a.DamagePerSecond).ThenByDescending(a => a.Strength); + afflictions.Where(a => !excludeBuffs || !a.Prefab.IsBuff).OrderByDescending(a => a.DamagePerSecond).ThenByDescending(a => a.Strength / a.Prefab.MaxStrength); public void Save(XElement healthElement) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs index c918b7363..da1952365 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs @@ -275,7 +275,8 @@ namespace Barotrauma Skills.Sort((x,y) => y.LevelRange.X.CompareTo(x.LevelRange.X)); - ClothingElement = element.GetChildElement("PortraitClothing"); + // Disabled on purpose, TODO: remove all references? + //ClothingElement = element.GetChildElement("PortraitClothing"); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index bf6179d52..fa7d525ae 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -744,7 +744,7 @@ namespace Barotrauma } if (attacker != null) { - var abilityAffliction = new AbilityAffliction(newAffliction); + var abilityAffliction = new AbilityAfflictionCharacter(newAffliction, character); attacker.CheckTalents(AbilityEffectType.OnAddDamageAffliction, abilityAffliction); } if (applyAffliction) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs index 649d9afcd..eb6290596 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs @@ -14,6 +14,7 @@ namespace Barotrauma NotDefined, Walk, Run, + Crouch, SwimSlow, SwimFast } @@ -56,12 +57,15 @@ namespace Barotrauma { [Serialize(25.0f, true, description: "Turning speed (or rather a force applied on the main collider to make it turn). Note that you can set a limb-specific steering forces too (additional)."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] public float SteerTorque { get; set; } + + [Serialize(25.0f, true, description: "How much torque is used to move the legs."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] + public float LegTorque { get; set; } } abstract class AnimationParams : EditableParams, IMemorizable { public string SpeciesName { get; private set; } - public bool IsGroundedAnimation => AnimationType == AnimationType.Walk || AnimationType == AnimationType.Run; + public bool IsGroundedAnimation => AnimationType == AnimationType.Walk || AnimationType == AnimationType.Run || AnimationType == AnimationType.Crouch; public bool IsSwimAnimation => AnimationType == AnimationType.SwimSlow || AnimationType == AnimationType.SwimFast; protected static Dictionary> allAnimations = new Dictionary>(); @@ -110,8 +114,18 @@ namespace Barotrauma } } } + public float TorsoAngleInRadians { get; private set; } = float.NaN; + [Serialize(50.0f, true, description: "How much torque is used to rotate the head to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] + public float HeadTorque { get; set; } + + [Serialize(50.0f, true, description: "How much torque is used to rotate the torso to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] + public float TorsoTorque { get; set; } + + [Serialize(25.0f, true, description: "How much torque is used to rotate the feet to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] + public float FootTorque { get; set; } + [Serialize(AnimationType.NotDefined, true), Editable] public virtual AnimationType AnimationType { get; protected set; } @@ -402,6 +416,8 @@ namespace Barotrauma return typeof(HumanWalkParams); case AnimationType.Run: return typeof(HumanRunParams); + case AnimationType.Crouch: + return typeof(HumanCrouchParams); case AnimationType.SwimSlow: return typeof(HumanSwimSlowParams); case AnimationType.SwimFast: diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs index 07c3bf980..c2257379b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs @@ -87,18 +87,9 @@ namespace Barotrauma [Serialize(8.0f, true, description: "How much force is used to move the feet to the correct position."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] public float FootMoveForce { get; set; } - [Serialize(50.0f, true, description: "How much torque is used to rotate the head to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] - public float HeadTorque { get; set; } - - [Serialize(50.0f, true, description: "How much torque is used to rotate the torso to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] - public float TorsoTorque { get; set; } - [Serialize(50.0f, true, description: "How much torque is used to rotate the tail to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] public float TailTorque { get; set; } - [Serialize(25.0f, true, description: "How much torque is used to rotate the feet to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] - public float FootTorque { get; set; } - [Serialize(0.0f, true, description: "Optional torque that's constantly applied to legs."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] public float LegTorque { get; set; } @@ -173,20 +164,12 @@ namespace Barotrauma [Editable, Serialize(true, true, description: "Should the character face towards the direction it's heading.")] public bool RotateTowardsMovement { get; set; } - [Serialize(25.0f, true, description: "How much torque is used to rotate the torso to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 2000, ValueStep = 1)] - public float TorsoTorque { get; set; } - - [Serialize(25.0f, true, description: "How much torque is used to rotate the head to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 2000, ValueStep = 1)] - public float HeadTorque { get; set; } - [Serialize(50.0f, true, description: "How much torque is used to rotate the tail to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 2000, ValueStep = 1)] public float TailTorque { get; set; } [Serialize(1f, true, description: "Multiplier applied based on the angle difference between the tail and the main limb. Increasing the value prevents snake-like characters from getting tangled on themselves. Default = 1 (no boost)"), Editable(MinValueFloat = 1, MaxValueFloat = 100)] public float TailTorqueMultiplier { get; set; } - [Serialize(25.0f, true, description: "How much torque is used to rotate the feet to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)] - public float FootTorque { get; set; } [Serialize(null, true), Editable] public string FootAngles @@ -224,10 +207,7 @@ namespace Barotrauma Dictionary FootAnglesInRadians { get; set; } float TailAngle { get; set; } float TailAngleInRadians { get; } - float HeadTorque { get; set; } - float TorsoTorque { get; set; } float TailTorque { get; set; } - float FootTorque { get; set; } bool Flip { get; set; } float FlipCooldown { get; set; } float FlipDelay { get; set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs index 639b7f46e..391c1fff8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/HumanoidAnimations.cs @@ -24,6 +24,17 @@ namespace Barotrauma public override void StoreSnapshot() => StoreSnapshot(); } + class HumanCrouchParams : HumanGroundedParams + { + public static HumanCrouchParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams(character, AnimationType.Crouch); + public static HumanCrouchParams GetAnimParams(Character character, string fileName = null) + { + return GetAnimParams(character.SpeciesName, AnimationType.Crouch, fileName); + } + + public override void StoreSnapshot() => StoreSnapshot(); + } + class HumanSwimFastParams: HumanSwimParams { public static HumanSwimFastParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams(character, AnimationType.SwimFast); @@ -58,9 +69,6 @@ namespace Barotrauma [Serialize("0.5, 0.1", true), Editable(DecimalCount = 2)] public Vector2 HandMoveAmount { get; set; } - [Serialize(0.5f, true), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] - public float HandMoveStrength { get; set; } - [Serialize(5.0f, true), Editable] public float HandCycleSpeed { get; set; } @@ -81,36 +89,23 @@ namespace Barotrauma } public float FootAngleInRadians { get; private set; } - [Serialize(25.0f, true, description: "How much torque is used to rotate the feet to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] - public float FootRotateStrength { get; set; } + [Serialize(1f, true, description: "How much force is used to move the arms."), Editable(MinValueFloat = 0, MaxValueFloat = 20, DecimalCount = 2)] + public float ArmMoveStrength { get; set; } + + [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 { [Serialize(0.3f, true, description: "How much force is used to force the character upright."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)] public float GetUpForce { get; set; } - - // -- TODO: use a separate clip for crawling -> replace these when implemented. - - [Serialize(0.65f, true, description: "Height of the torso when crouching."), Editable(MinValueFloat = 0, MaxValueFloat = 5, DecimalCount = 2)] - public float CrouchingTorsoPos { get; set; } - - [Serialize(0.65f, true, description: "Height of the head when crouching."), Editable(MinValueFloat = 0, MaxValueFloat = 5, DecimalCount = 2)] - public float CrouchingHeadPos { get; set; } - - /// - /// In degrees - /// - [Serialize(-10f, true, description: "Angle of the torso when crouching."), Editable(MinValueFloat = -360, MaxValueFloat = 360)] - public float CrouchingTorsoAngle { get; set; } - - /// - /// In degrees - /// - [Serialize(-10f, true, description: "Angle of the head when crouching."), Editable(MinValueFloat = -360, MaxValueFloat = 360)] - public float CrouchingHeadAngle { get; set; } - - // -- [Serialize(0.25f, true, description: "How much the character's head leans forwards when moving."), Editable(DecimalCount = 2)] public float HeadLeanAmount { get; set; } @@ -121,6 +116,9 @@ namespace Barotrauma [Serialize(15.0f, true, description: "How much force is used to move the feet to the correct position."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] public float FootMoveStrength { get; set; } + [Serialize(0f, true, description: "How much the horizontal difference of waist and the foot positions has an effect to lifting the foot."), Editable(DecimalCount = 2, ValueStep = 0.1f, MinValueFloat = 0f, MaxValueFloat = 1f)] + public float FootLiftHorizontalFactor { get; set; } + /// /// In degrees. /// @@ -135,15 +133,9 @@ namespace Barotrauma } public float FootAngleInRadians { get; private set; } - [Serialize(20.0f, true, description: "How much torque is used to rotate the feet to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] - public float FootRotateStrength { get; set; } - [Serialize("0.0, 0.0", true, description: "Added to the calculated foot positions, e.g. a value of {-1.0, 0.0f} would make the character \"drag\" their feet one unit behind them."), Editable(DecimalCount = 2)] public Vector2 FootMoveOffset { get; set; } - [Serialize("0.0, 0.0", true, description: "Added to the calculated foot positions, e.g. a value of {-1.0, 0.0f} would make the character \"drag\" their feet one unit behind them."), Editable(DecimalCount = 2)] - public Vector2 CrouchingFootMoveOffset { get; set; } - [Serialize(10.0f, true, description: "How much torque is used to bend the characters legs when taking a step."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] public float LegBendTorque { get; set; } @@ -153,17 +145,33 @@ namespace Barotrauma [Serialize("-0.15, 0.0", true, description: "Added to the calculated hand positions, e.g. a value of {-1.0, 0.0f} would make the character \"drag\" their hands one unit behind them."), Editable(DecimalCount = 2)] public Vector2 HandMoveOffset { get; set; } - [Serialize(0.7f, true, description: "How much force is used to move the hands."), Editable(MinValueFloat = 0, MaxValueFloat = 2, DecimalCount = 2)] - public float HandMoveStrength { get; set; } - [Serialize(-1.0f, true, description: "The position of the hands is clamped below this (relative to the position of the character's torso)."), Editable(DecimalCount = 2)] public float HandClampY { get; set; } + + [Serialize(1f, true, description: "How much force is used to move the arms."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] + public float ArmMoveStrength { get; set; } + + [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 { float FootAngle { get; set; } float FootAngleInRadians { get; } - float FootRotateStrength { get; set; } + + float ArmMoveStrength { get; set; } + + float HandMoveStrength { get; set; } + + float ArmIKStrength { get; set; } + + float HandIKStrength { get; set; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs index 9c41dddb0..a5be463ca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs @@ -34,6 +34,9 @@ namespace Barotrauma [Serialize("", true, description: "Default path for the limb sprite textures. Used only if the limb specific path for the limb is not defined"), Editable] public string Texture { get; set; } + [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)] public float SpritesheetOrientation { get; set; } @@ -556,7 +559,7 @@ namespace Barotrauma } } - public override string GenerateName() => $"Limb {ID}"; + public override string GenerateName() => Type != LimbType.None ? $"{Type} ({ID})" : $"Limb {ID}"; public SpriteParams GetSprite() => deformSpriteParams ?? normalSpriteParams; @@ -574,7 +577,7 @@ namespace Barotrauma [Serialize("", true), Editable] public string Notes { get; set; } - [Serialize(1f, true), Editable] + [Serialize(1f, true), Editable(DecimalCount = 2)] public float Scale { get; set; } [Serialize(true, true, description: "Does the limb flip when the character flips?"), Editable()] @@ -889,6 +892,9 @@ namespace Barotrauma [Serialize("", true), Editable()] public string Texture { get; set; } + [Serialize(false, true), Editable()] + public bool IgnoreTint { get; set; } + [Serialize("1.0,1.0,1.0,1.0", true), Editable()] public Color Color { get; set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityCondition.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityCondition.cs index 4bdddab03..959cf4148 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityCondition.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityCondition.cs @@ -33,6 +33,7 @@ namespace Barotrauma.Abilities NotSelf = 3, Alive = 4, Monster = 5, + InFriendlySubmarine = 6, }; protected List ParseTargetTypes(string[] targetTypeStrings) @@ -80,6 +81,8 @@ namespace Barotrauma.Abilities return !targetCharacter.IsDead; case TargetType.Monster: return !targetCharacter.IsHuman; + case TargetType.InFriendlySubmarine: + return targetCharacter.Submarine != null && targetCharacter.Submarine.TeamID == character.TeamID; default: return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAffliction.cs new file mode 100644 index 000000000..4d909aa81 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAffliction.cs @@ -0,0 +1,29 @@ +using Barotrauma.Items.Components; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionAffliction : AbilityConditionData + { + private readonly string[] afflictions; + public AbilityConditionAffliction(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + afflictions = conditionElement.GetAttributeStringArray("afflictions", new string[0], convertToLowerInvariant: true); + } + + protected override bool MatchesConditionSpecific(AbilityObject abilityObject) + { + if ((abilityObject as IAbilityAffliction)?.Affliction is Affliction affliction) + { + return afflictions.Any(a => a == affliction.Identifier); + } + else + { + LogAbilityConditionError(abilityObject, typeof(IAbilityAttackResult)); + return false; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionLocation.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionLocation.cs new file mode 100644 index 000000000..b2d70b0b3 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionLocation.cs @@ -0,0 +1,41 @@ +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionLocation : AbilityConditionData + { + private readonly bool? hasOutpost; + private readonly string[] locationIdentifiers; + + public AbilityConditionLocation(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + if (conditionElement.Attribute("hasoutpost") != null) + { + hasOutpost = conditionElement.GetAttributeBool("hasoutpost", false); + } + locationIdentifiers = conditionElement.GetAttributeStringArray("locationtype", new string[0]); + } + + protected override bool MatchesConditionSpecific(AbilityObject abilityObject) + { + if (abilityObject is IAbilityLocation abilityLocation) + { + if (locationIdentifiers.Any()) + { + if (!locationIdentifiers.Contains(abilityLocation.Location.Type.Identifier)) { return false; } + } + if (hasOutpost.HasValue) + { + if (hasOutpost.Value != abilityLocation.Location.HasOutpost()) { return false; } + } + return true; + } + else + { + LogAbilityConditionError(abilityObject, typeof(IAbilityItemPrefab)); + return false; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityInterfaces.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityInterfaces.cs index 5a169edf3..8c552ad82 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityInterfaces.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityInterfaces.cs @@ -20,6 +20,11 @@ public Mission Mission { get; set; } } + interface IAbilityLocation + { + public Location Location { get; set; } + } + interface IAbilityCharacter { public Character Character { get; set; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityObjects.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityObjects.cs index 292be7b94..6939b18af 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityObjects.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityObjects.cs @@ -43,6 +43,17 @@ namespace Barotrauma.Abilities public Affliction Affliction { get; set; } } + class AbilityAfflictionCharacter : AbilityObject, IAbilityAffliction, IAbilityCharacter + { + public AbilityAfflictionCharacter(Affliction affliction, Character character) + { + Affliction = affliction; + Character = character; + } + public Character Character { get; set; } + public Affliction Affliction { get; set; } + } + class AbilityValueItem : AbilityObject, IAbilityValue, IAbilityItemPrefab { public AbilityValueItem(float value, ItemPrefab itemPrefab) @@ -54,6 +65,17 @@ namespace Barotrauma.Abilities public ItemPrefab ItemPrefab { get; set; } } + class AbilityItemPrefabItem : AbilityObject, IAbilityItem, IAbilityItemPrefab + { + public AbilityItemPrefabItem(Item item, ItemPrefab itemPrefab) + { + Item = item; + ItemPrefab = itemPrefab; + } + public Item Item { get; set; } + public ItemPrefab ItemPrefab { get; set; } + } + class AbilityValueString : AbilityObject, IAbilityValue, IAbilityString { public AbilityValueString(float value, string abilityString) @@ -65,7 +87,7 @@ namespace Barotrauma.Abilities public string String { get; set; } } - class AbilityValueStringCharacter : AbilityObject, IAbilityValue, IAbilityString + class AbilityValueStringCharacter : AbilityObject, IAbilityValue, IAbilityString, IAbilityCharacter { public AbilityValueStringCharacter(float value, string abilityString, Character character) { @@ -111,6 +133,16 @@ namespace Barotrauma.Abilities public Mission Mission { get; set; } } + class AbilityLocation : AbilityObject, IAbilityLocation + { + public AbilityLocation(Location location) + { + Location = location; + } + + public Location Location { get; set; } + } + // this is an exception class that should only be passed in this form, so classes that use it should cast into it directly class AbilityAttackData : AbilityObject, IAbilityCharacter { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAllies.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAllies.cs index 2b1115068..9cbaa17eb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAllies.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAllies.cs @@ -1,5 +1,4 @@ -using Barotrauma.Extensions; -using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; @@ -9,10 +8,12 @@ namespace Barotrauma.Abilities class CharacterAbilityApplyStatusEffectsToAllies : CharacterAbilityApplyStatusEffects { private readonly bool allowSelf; + private readonly float maxDistance = float.MaxValue; public CharacterAbilityApplyStatusEffectsToAllies(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) { allowSelf = abilityElement.GetAttributeBool("allowself", true); + maxDistance = abilityElement.GetAttributeFloat("maxdistance", float.MaxValue); } @@ -22,6 +23,10 @@ namespace Barotrauma.Abilities foreach (Character character in chosenCharacters) { + if (maxDistance < float.MaxValue) + { + if (Vector2.DistanceSquared(character.WorldPosition, Character.WorldPosition) > maxDistance * maxDistance) { continue; } + } ApplyEffectSpecific(character); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMoney.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMoney.cs index a10d132c5..d02647357 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMoney.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMoney.cs @@ -1,5 +1,4 @@ -using Microsoft.Xna.Framework; -using System.Xml.Linq; +using System.Xml.Linq; namespace Barotrauma.Abilities { @@ -8,7 +7,7 @@ namespace Barotrauma.Abilities public override bool AppliesEffectOnIntervalUpdate => true; private readonly int amount; - private StatTypes scalingStatType; + private readonly StatTypes scalingStatType; public CharacterAbilityGiveMoney(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs index ea1a181a2..dce3ed4f8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs @@ -13,6 +13,7 @@ namespace Barotrauma.Abilities private readonly bool removeOnDeath; private readonly bool removeAfterRound; private readonly bool giveOnAddingFirstTime; + private readonly bool setValue; //private readonly float maximumValue; @@ -28,6 +29,7 @@ namespace Barotrauma.Abilities removeOnDeath = abilityElement.GetAttributeBool("removeondeath", true); removeAfterRound = abilityElement.GetAttributeBool("removeafterround", false); giveOnAddingFirstTime = abilityElement.GetAttributeBool("giveonaddingfirsttime", characterAbilityGroup.AbilityEffectType == AbilityEffectType.None); + setValue = abilityElement.GetAttributeBool("setvalue", false); } public override void InitializeAbility(bool addingFirstTime) @@ -52,11 +54,11 @@ namespace Barotrauma.Abilities { if (targetAllies) { - Character.GetFriendlyCrew(Character).ForEach(c => c?.Info.ChangeSavedStatValue(statType, value, statIdentifier, removeOnDeath, removeAfterRound, maxValue)); + Character.GetFriendlyCrew(Character).ForEach(c => c?.Info.ChangeSavedStatValue(statType, value, statIdentifier, removeOnDeath, removeAfterRound, maxValue, setValue)); } else { - Character?.Info.ChangeSavedStatValue(statType, value, statIdentifier, removeOnDeath, removeAfterRound, maxValue); + Character?.Info.ChangeSavedStatValue(statType, value, statIdentifier, removeOnDeath, removeAfterRound, maxValue, setValue); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveResistance.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveResistance.cs index 707d31449..af08e66bc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveResistance.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveResistance.cs @@ -9,7 +9,7 @@ namespace Barotrauma.Abilities public CharacterAbilityGiveResistance(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) { - resistanceId = abilityElement.GetAttributeString("resistanceid", ""); + resistanceId = abilityElement.GetAttributeString("resistanceid", abilityElement.GetAttributeString("resistance", string.Empty)); multiplier = abilityElement.GetAttributeFloat("multiplier", 1f); if (string.IsNullOrEmpty(resistanceId)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToLevel.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToLevel.cs new file mode 100644 index 000000000..a8630dc2c --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToLevel.cs @@ -0,0 +1,35 @@ +using Microsoft.Xna.Framework; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityModifyStatToLevel : CharacterAbility + { + private readonly StatTypes statType; + private readonly float statPerLevel; + private readonly int maxLevel; + private float lastValue = 0f; + + public CharacterAbilityModifyStatToLevel(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + statType = CharacterAbilityGroup.ParseStatType(abilityElement.GetAttributeString("stattype", ""), CharacterTalent.DebugIdentifier); + statPerLevel = abilityElement.GetAttributeFloat("statperlevel", 0f); + maxLevel = abilityElement.GetAttributeInt("maxlevel", int.MaxValue); + } + + protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate) + { + Character.ChangeStat(statType, -lastValue); + if (conditionsMatched) + { + int level = MathHelper.Min(Character?.Info.GetCurrentLevel() ?? 0, maxLevel); + lastValue = statPerLevel * level; + Character.ChangeStat(statType, lastValue); + } + else + { + lastValue = 0f; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySpawnItemsToContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySpawnItemsToContainer.cs index d092e972c..b13e97638 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySpawnItemsToContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySpawnItemsToContainer.cs @@ -10,19 +10,24 @@ namespace Barotrauma.Abilities private readonly List statusEffects; private readonly List openedContainers = new List(); private readonly float randomChance; + private readonly bool oncePerContainer; public CharacterAbilitySpawnItemsToContainer(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) { statusEffects = CharacterAbilityGroup.ParseStatusEffects(CharacterTalent, abilityElement.GetChildElement("statuseffects")); randomChance = abilityElement.GetAttributeFloat("randomchance", 1f); + oncePerContainer = abilityElement.GetAttributeBool("oncepercontainer", false); } protected override void ApplyEffect(AbilityObject abilityObject) { if ((abilityObject as IAbilityItem)?.Item is Item item) { - if (openedContainers.Contains(item)) { return; } - openedContainers.Add(item); + if (oncePerContainer) + { + if (openedContainers.Contains(item)) { return; } + openedContainers.Add(item); + } if (randomChance < Rand.Range(0f, 1f, Rand.RandSync.Unsynced)) { return; } foreach (var statusEffect in statusEffects) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityUnlockTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityUnlockTree.cs new file mode 100644 index 000000000..c9a5f9364 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityUnlockTree.cs @@ -0,0 +1,30 @@ +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityUnlockTree : CharacterAbility + { + public CharacterAbilityUnlockTree(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + } + + public override void InitializeAbility(bool addingFirstTime) + { + if (!addingFirstTime) { return; } + if (!TalentTree.JobTalentTrees.TryGetValue(Character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return; } + + var subTree = talentTree.TalentSubTrees.Find(t => t.TalentOptionStages.Any(ts => ts.Talents.Contains(CharacterTalent.Prefab))); + if (subTree != null) + { + foreach (var talentOption in subTree.TalentOptionStages) + { + foreach (var talent in talentOption.Talents) + { + Character.GiveTalent(talent); + } + } + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityEnigmaMachine.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAtmosMachine.cs similarity index 86% rename from Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityEnigmaMachine.cs rename to Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAtmosMachine.cs index 6b034d24b..5ab9361b8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityEnigmaMachine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAtmosMachine.cs @@ -4,14 +4,14 @@ using System.Xml.Linq; namespace Barotrauma.Abilities { - class CharacterAbilityEnigmaMachine : CharacterAbility + class CharacterAbilityAtmosMachine : CharacterAbility { private readonly float addedValue; private readonly float multiplyValue; private readonly string[] tags; private readonly int maxMultiplyCount; - public CharacterAbilityEnigmaMachine(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + public CharacterAbilityAtmosMachine(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) { addedValue = abilityElement.GetAttributeFloat("addedvalue", 0f); multiplyValue = abilityElement.GetAttributeFloat("multiplyvalue", 1f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityStonewall.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityStonewall.cs deleted file mode 100644 index cc2aa51c0..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityStonewall.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Microsoft.Xna.Framework; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; - -namespace Barotrauma.Abilities -{ - class CharacterAbilityStonewall : CharacterAbility - { - private readonly List statusEffects; - private readonly List statusEffectsReset; - private readonly int maxEnemyCount; - private readonly float squaredDistance; - - public CharacterAbilityStonewall(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) - { - statusEffects = CharacterAbilityGroup.ParseStatusEffects(CharacterTalent, abilityElement.GetChildElement("statuseffects")); - statusEffectsReset = CharacterAbilityGroup.ParseStatusEffects(CharacterTalent, abilityElement.GetChildElement("statuseffectsreset")); - maxEnemyCount = abilityElement.GetAttributeInt("maxenemycount", 0); - squaredDistance = MathF.Pow(abilityElement.GetAttributeFloat("distance", 0), 2); - } - - protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate) - { - int numberOfEnemiesInRange = Character.CharacterList.Count(c => !HumanAIController.IsFriendly(Character, c) && !c.IsDead && Vector2.DistanceSquared(Character.WorldPosition, c.WorldPosition) < squaredDistance); - - foreach (var statusEffect in statusEffectsReset) - { - statusEffect.Apply(ActionType.OnAbility, 1f, Character, Character); - } - - if (conditionsMatched && numberOfEnemiesInRange > 0) - { - foreach (var statusEffect in statusEffects) - { - statusEffect.Apply(ActionType.OnAbility, Math.Min(numberOfEnemiesInRange, maxEnemyCount), Character, Character); - } - } - } - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupEffect.cs index 7249b69bf..e4d488103 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupEffect.cs @@ -23,6 +23,7 @@ namespace Barotrauma.Abilities characterAbility.ApplyAbilityEffect(abilityObject); } } + timesTriggered++; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs index 379b09f46..c7a302149 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs @@ -39,6 +39,10 @@ namespace Barotrauma.Abilities characterAbility.UpdateCharacterAbility(conditionsMatched, TimeSinceLastUpdate); } } + if (conditionsMatched) + { + timesTriggered++; + } TimeSinceLastUpdate = 0; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs index 744ffb132..92736e1d1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs @@ -34,7 +34,7 @@ namespace Barotrauma if (string.IsNullOrEmpty(jobIdentifier)) { - DebugConsole.ThrowError("No job defined for talent tree!"); + DebugConsole.ThrowError($"No job defined for talent tree in \"{filePath}\"!"); return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs index 20acddd27..90d564ef1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs @@ -56,9 +56,10 @@ OnAllyGainMissionExperience, OnGainMissionExperience, OnGainMissionMoney, + OnLocationDiscovered, OnItemDeconstructed, OnItemDeconstructedMaterial, - OnItemDeconstructedRetainProbability, + OnItemDeconstructedInventory, OnStopTinkering, OnItemPicked, AfterSubmarineAttacked, @@ -96,7 +97,6 @@ // Utility RepairSpeed, DeconstructorSpeedMultiplier, - TinkeringDuration, RepairToolStructureRepairMultiplier, RepairToolStructureDamageMultiplier, RepairToolDeattachTimeMultiplier, @@ -105,6 +105,10 @@ GeneticMaterialRefineBonus, GeneticMaterialTaintedProbabilityReductionOnCombine, SkillGainSpeed, + // Tinker + TinkeringDuration, + TinkeringStrength, + TinkeringDamage, // Misc ReputationGainMultiplier, MissionMoneyGainMultiplier, @@ -114,6 +118,7 @@ Coauthor, WarriorPoetMissionRuns, WarriorPoetEnemiesKilled, + QuickfixRepairCount, } public enum AbilityFlags diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index 49b16f9a3..ee63377eb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -868,6 +868,7 @@ namespace Barotrauma { if (level == null) { return 0.0f; } var refEntity = GetRefEntity(); + if (refEntity == null) { return 0.0f; } Vector2 target = ConvertUnits.ToSimUnits(level.EndPosition); var steeringPath = pathFinder.FindPath(ConvertUnits.ToSimUnits(refEntity.WorldPosition), target); if (steeringPath.Unreachable || float.IsPositiveInfinity(totalPathLength)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs index 04278f4d6..e407ba68d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MineralMission.cs @@ -9,10 +9,10 @@ namespace Barotrauma { partial class MineralMission : Mission { - private Dictionary> ResourceClusters { get; } = new Dictionary>(); - private Dictionary> SpawnedResources { get; } = new Dictionary>(); - private Dictionary RelevantLevelResources { get; } = new Dictionary(); - private List> MissionClusterPositions { get; } = new List>(); + private readonly Dictionary resourceClusters = new Dictionary(); + private readonly Dictionary> spawnedResources = new Dictionary>(); + private readonly Dictionary relevantLevelResources = new Dictionary(); + private readonly List> missionClusterPositions = new List>(); private readonly HashSet caves = new HashSet(); @@ -20,8 +20,8 @@ namespace Barotrauma { get { - return MissionClusterPositions - .Where(p => SpawnedResources.ContainsKey(p.Item1) && AnyAreUncollected(SpawnedResources[p.Item1])) + return missionClusterPositions + .Where(p => spawnedResources.ContainsKey(p.Item1) && AnyAreUncollected(spawnedResources[p.Item1])) .Select(p => p.Item2); } } @@ -33,53 +33,53 @@ namespace Barotrauma { var identifier = c.GetAttributeString("identifier", null); if (string.IsNullOrWhiteSpace(identifier)) { continue; } - if (ResourceClusters.ContainsKey(identifier)) + if (resourceClusters.ContainsKey(identifier)) { - ResourceClusters[identifier].First++; + resourceClusters[identifier] = (resourceClusters[identifier].amount + 1, resourceClusters[identifier].rotation); } else { - ResourceClusters.Add(identifier, new Pair(1, 0.0f)); + resourceClusters.Add(identifier, (1, 0.0f)); } } } protected override void StartMissionSpecific(Level level) { - if (SpawnedResources.Any()) + if (spawnedResources.Any()) { #if DEBUG - throw new Exception($"SpawnedResources.Count > 0 ({SpawnedResources.Count})"); + throw new Exception($"SpawnedResources.Count > 0 ({spawnedResources.Count})"); #else DebugConsole.AddWarning("Spawned resources list was not empty at the start of a mineral mission. The mission instance may not have been ended correctly on previous rounds."); - SpawnedResources.Clear(); + spawnedResources.Clear(); #endif } - if (RelevantLevelResources.Any()) + if (relevantLevelResources.Any()) { #if DEBUG - throw new Exception($"RelevantLevelResources.Count > 0 ({RelevantLevelResources.Count})"); + throw new Exception($"RelevantLevelResources.Count > 0 ({relevantLevelResources.Count})"); #else DebugConsole.AddWarning("Relevant level resources list was not empty at the start of a mineral mission. The mission instance may not have been ended correctly on previous rounds."); - RelevantLevelResources.Clear(); + relevantLevelResources.Clear(); #endif } - if (MissionClusterPositions.Any()) + if (missionClusterPositions.Any()) { #if DEBUG - throw new Exception($"MissionClusterPositions.Count > 0 ({MissionClusterPositions.Count})"); + throw new Exception($"MissionClusterPositions.Count > 0 ({missionClusterPositions.Count})"); #else DebugConsole.AddWarning("Mission cluster positions list was not empty at the start of a mineral mission. The mission instance may not have been ended correctly on previous rounds."); - MissionClusterPositions.Clear(); + missionClusterPositions.Clear(); #endif } caves.Clear(); if (IsClient) { return; } - foreach (var kvp in ResourceClusters) + foreach (var kvp in resourceClusters) { var prefab = ItemPrefab.Find(null, kvp.Key); if (prefab == null) @@ -88,15 +88,14 @@ namespace Barotrauma "couldn't find an item prefab with the identifier " + kvp.Key); continue; } - var spawnedResources = level.GenerateMissionResources(prefab, kvp.Value.First, out float rotation); - if (spawnedResources.Count < kvp.Value.First) + var spawnedResources = level.GenerateMissionResources(prefab, kvp.Value.amount, out float rotation); + if (spawnedResources.Count < kvp.Value.amount) { DebugConsole.ThrowError("Error in MineralMission - " + - "spawned " + spawnedResources.Count + "/" + kvp.Value.First + " of " + prefab.Name); + "spawned " + spawnedResources.Count + "/" + kvp.Value.amount + " of " + prefab.Name); } if (spawnedResources.None()) { continue; } - SpawnedResources.Add(kvp.Key, spawnedResources); - kvp.Value.Second = rotation; + this.spawnedResources.Add(kvp.Key, spawnedResources); foreach (Level.Cave cave in Level.Loaded.Caves) { @@ -142,7 +141,7 @@ namespace Barotrauma GiveReward(); completed = true; } - foreach (var kvp in SpawnedResources) + foreach (var kvp in spawnedResources) { foreach (var i in kvp.Value) { @@ -152,33 +151,33 @@ namespace Barotrauma } } } - SpawnedResources.Clear(); - RelevantLevelResources.Clear(); - MissionClusterPositions.Clear(); + spawnedResources.Clear(); + relevantLevelResources.Clear(); + missionClusterPositions.Clear(); failed = !completed && state > 0; } private void FindRelevantLevelResources() { - RelevantLevelResources.Clear(); - foreach (var identifier in ResourceClusters.Keys) + relevantLevelResources.Clear(); + foreach (var identifier in resourceClusters.Keys) { var items = Item.ItemList.Where(i => i.Prefab.Identifier == identifier && i.Submarine == null && i.ParentInventory == null && (!(i.GetComponent() is Holdable h) || (h.Attachable && h.Attached))) .ToArray(); - RelevantLevelResources.Add(identifier, items); + relevantLevelResources.Add(identifier, items); } } private bool EnoughHaveBeenCollected() { - foreach (var kvp in ResourceClusters) + foreach (var kvp in resourceClusters) { - if (RelevantLevelResources.TryGetValue(kvp.Key, out var availableResources)) + if (relevantLevelResources.TryGetValue(kvp.Key, out var availableResources)) { var collected = availableResources.Count(r => HasBeenCollected(r)); - var needed = kvp.Value.First; + var needed = kvp.Value.amount; if (collected < needed) { return false; } } else @@ -210,8 +209,8 @@ namespace Barotrauma private void CalculateMissionClusterPositions() { - MissionClusterPositions.Clear(); - foreach (var kvp in SpawnedResources) + missionClusterPositions.Clear(); + foreach (var kvp in spawnedResources) { if (kvp.Value.None()) { continue; } var pos = Vector2.Zero; @@ -222,7 +221,7 @@ namespace Barotrauma itemCount++; } pos /= itemCount; - MissionClusterPositions.Add(new Tuple(kvp.Key, pos)); + missionClusterPositions.Add(new Tuple(kvp.Key, pos)); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs index 2374eefea..e9d3cef40 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs @@ -180,7 +180,11 @@ namespace Barotrauma var path = pathFinder.FindPath(ConvertUnits.ToSimUnits(patrolPos), ConvertUnits.ToSimUnits(preferredSpawnPos)); if (!path.Unreachable) { - preferredSpawnPos = path.Nodes[Rand.Range(0, path.Nodes.Count - 1)].WorldPosition; // spawn the sub in a random point in the path if possible + var validNodes = path.Nodes.FindAll(n => !Level.Loaded.ExtraWalls.Any(w => w.Cells.Any(c => c.IsPointInside(n.WorldPosition)))); + if (validNodes.Any()) + { + preferredSpawnPos = validNodes.GetRandom().WorldPosition; // spawn the sub in a random point in the path if possible + } } int graceDistance = 500; // the sub still spawns awkwardly close to walls, so this helps. could also be given as a parameter instead diff --git a/Barotrauma/BarotraumaClient/ClientSource/Extensions/ColorExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Extensions/ColorExtensions.cs similarity index 69% rename from Barotrauma/BarotraumaClient/ClientSource/Extensions/ColorExtensions.cs rename to Barotrauma/BarotraumaShared/SharedSource/Extensions/ColorExtensions.cs index 3de8f949c..89fc4cefa 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Extensions/ColorExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Extensions/ColorExtensions.cs @@ -1,5 +1,4 @@ using Microsoft.Xna.Framework; -using System; namespace Barotrauma.Extensions { @@ -12,6 +11,11 @@ namespace Barotrauma.Extensions new Color((byte)(color.R * value), (byte)(color.G * value), (byte)(color.B * value), (byte)(color.A * value)); } + public static Color Multiply(this Color thisColor, Color color) + { + return new Color((byte)(thisColor.R * color.R / 255f), (byte)(thisColor.G * color.G / 255f), (byte)(thisColor.B * color.B / 255f), (byte)(thisColor.A * color.A / 255f)); + } + public static Color Opaque(this Color color) { return new Color(color.R, color.G, color.B, (byte)255); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index b1e2c79e8..5d89b421e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -659,16 +659,7 @@ namespace Barotrauma } foreach (Location location in Map.Locations) { - if (location.Type != location.OriginalType) - { - location.ChangeType(location.OriginalType); - location.PendingLocationTypeChange = null; - } - location.CreateStore(force: true); - location.ClearMissions(); - location.Discovered = false; - location.LevelData?.EventHistory?.Clear(); - location.UnlockInitialMissions(); + location.Reset(); } Map.SetLocation(Map.Locations.IndexOf(Map.StartLocation)); Map.SelectLocation(-1); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index cb791155d..afdaf91e6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -452,9 +452,11 @@ namespace Barotrauma StatusEffect.StopAll(); #if CLIENT +#if !DEBUG GameMain.LightManager.LosEnabled = GameMain.Client == null || GameMain.Client.CharacterInfo != null; +#endif if (GameMain.LightManager.LosEnabled) { GameMain.LightManager.LosAlpha = 1f; } - if (GameMain.Client == null) GameMain.LightManager.LosMode = GameMain.Config.LosMode; + if (GameMain.Client == null) { GameMain.LightManager.LosMode = GameMain.Config.LosMode; } #endif LevelData = level?.LevelData; Level = level; @@ -661,6 +663,7 @@ namespace Barotrauma #if SERVER return GameMain.Server.ConnectedClients.Select(c => c.Character).Where(c => c?.Info != null); #else + if (GameMain.GameSession == null) { return Enumerable.Empty(); } return GameMain.GameSession.CrewManager.GetCharacters().Where(c => c?.Info != null); #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs index 2183d6866..40479d07b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs @@ -143,14 +143,7 @@ namespace Barotrauma return true; } - public int CharacterHeadIndex { get; set; } - public int CharacterHairIndex { get; set; } - public int CharacterBeardIndex { get; set; } - public int CharacterMoustacheIndex { get; set; } - public int CharacterFaceAttachmentIndex { get; set; } - - public Gender CharacterGender { get; set; } - public Race CharacterRace { get; set; } + internal CharacterInfo.HeadInfo PlayerCharacterCustomization { get; set; } private float aimAssistAmount; public float AimAssistAmount @@ -855,171 +848,6 @@ namespace Barotrauma UnsavedSettings = false; } - private void SaveNewDefaultConfig() - { - XDocument doc = new XDocument(); - - if (doc.Root == null) - { - doc.Add(new XElement("config")); - } - - doc.Root.Add( - new XAttribute("language", TextManager.Language), - new XAttribute("masterserverurl", MasterServerUrl), - new XAttribute("remotecontenturl", RemoteContentUrl), - new XAttribute("autocheckupdates", AutoCheckUpdates), - new XAttribute("musicvolume", musicVolume), - new XAttribute("soundvolume", soundVolume), - new XAttribute("microphonevolume", microphoneVolume), - new XAttribute("voicechatvolume", voiceChatVolume), - new XAttribute("voicechatcutoffprevention", VoiceChatCutoffPrevention), - new XAttribute("verboselogging", VerboseLogging), - new XAttribute("savedebugconsolelogs", SaveDebugConsoleLogs), - new XAttribute("submarineautosave", EnableSubmarineAutoSave), - new XAttribute("maxautosaves", MaximumAutoSaves), - new XAttribute("autosaveintervalseconds", AutoSaveIntervalSeconds), - new XAttribute("subeditorbackground", XMLExtensions.ColorToString(SubEditorBackgroundColor)), - new XAttribute("subeditorundobuffer", SubEditorMaxUndoBuffer), - new XAttribute("enablesplashscreen", EnableSplashScreen), - new XAttribute("usesteammatchmaking", UseSteamMatchmaking), - new XAttribute("quickstartsub", QuickStartSubmarineName), - new XAttribute("requiresteamauthentication", RequireSteamAuthentication), - new XAttribute("aimassistamount", aimAssistAmount), - new XAttribute("tutorialskipwarning", ShowTutorialSkipWarning)); - - if (!ShowUserStatisticsPrompt) - { - doc.Root.Add(new XAttribute("senduserstatistics", sendUserStatistics)); - } - - XElement gMode = doc.Root.Element("graphicsmode"); - if (gMode == null) - { - gMode = new XElement("graphicsmode"); - doc.Root.Add(gMode); - } - if (GraphicsWidth == 0 || GraphicsHeight == 0) - { - gMode.ReplaceAttributes(new XAttribute("displaymode", windowMode)); - } - else - { - gMode.ReplaceAttributes( - new XAttribute("width", GraphicsWidth), - new XAttribute("height", GraphicsHeight), - new XAttribute("vsync", VSyncEnabled), - new XAttribute("framelimit", Timing.FrameLimit), - new XAttribute("displaymode", windowMode)); - } - - XElement gSettings = doc.Root.Element("graphicssettings"); - if (gSettings == null) - { - gSettings = new XElement("graphicssettings"); - doc.Root.Add(gSettings); - } - - gSettings.ReplaceAttributes( - new XAttribute("particlelimit", ParticleLimit), - new XAttribute("lightmapscale", LightMapScale), - new XAttribute("chromaticaberration", ChromaticAberrationEnabled), - new XAttribute("losmode", LosMode), - new XAttribute("hudscale", HUDScale), - new XAttribute("inventoryscale", InventoryScale)); - - foreach (ContentPackage contentPackage in ContentPackage.CorePackages) - { - if (contentPackage.Path.Contains(VanillaContentPackagePath)) - { - doc.Root.Add(new XElement("contentpackages", new XElement("core", new XAttribute("name", contentPackage.Name)))); - break; - } - } - -#if CLIENT - var keyMappingElement = new XElement("keymapping"); - doc.Root.Add(keyMappingElement); - for (int i = 0; i < keyMapping.Length; i++) - { - KeyOrMouse bind = keyMapping[i]; - if (bind.MouseButton == MouseButton.None) - { - keyMappingElement.Add(new XAttribute(((InputType)i).ToString(), bind.Key)); - } - else - { - keyMappingElement.Add(new XAttribute(((InputType)i).ToString(), bind.MouseButton)); - } - } - - var inventoryKeyMappingElement = new XElement("inventorykeymapping"); - doc.Root.Add(inventoryKeyMappingElement); - for (int i = 0; i < inventoryKeyMapping.Length; i++) - { - KeyOrMouse bind = inventoryKeyMapping[i]; - if (bind.MouseButton == MouseButton.None) - { - inventoryKeyMappingElement.Add(new XAttribute($"slot{i}", bind.Key)); - } - else - { - inventoryKeyMappingElement.Add(new XAttribute($"slot{i}", bind.MouseButton)); - } - } -#endif - - var gameplay = new XElement("gameplay"); - var jobPreferences = new XElement("jobpreferences"); - foreach (Pair job in JobPreferences) - { - XElement jobElement = new XElement("job"); - jobElement.Add(new XAttribute("identifier", job.First)); - jobElement.Add(new XAttribute("variant", job.Second)); - jobPreferences.Add(jobElement); - } - gameplay.Add(jobPreferences); - - var teamPreference = new XElement("teampreference"); - teamPreference.Add(new XAttribute("team", TeamPreference.ToString())); - gameplay.Add(teamPreference); - - doc.Root.Add(gameplay); - - var playerElement = new XElement("player", - new XAttribute("name", playerName ?? ""), - new XAttribute("headindex", CharacterHeadIndex), - new XAttribute("gender", CharacterGender), - new XAttribute("race", CharacterRace), - new XAttribute("hairindex", CharacterHairIndex), - new XAttribute("beardindex", CharacterBeardIndex), - new XAttribute("moustacheindex", CharacterMoustacheIndex), - new XAttribute("faceattachmentindex", CharacterFaceAttachmentIndex)); - doc.Root.Add(playerElement); - - System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings - { - Indent = true, - OmitXmlDeclaration = true, - NewLineOnAttributes = true - }; - - try - { - using (var writer = XmlWriter.Create(SavePath, settings)) - { - doc.WriteTo(writer); - writer.Flush(); - } - } - catch (Exception e) - { - DebugConsole.ThrowError("Saving game settings failed.", e); - GameAnalyticsManager.AddErrorEventOnce("GameSettings.Save:SaveFailed", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - "Saving game settings failed.\n" + e.Message + "\n" + e.StackTrace.CleanupStackTrace()); - } - } - #region Load PlayerConfig public void LoadPlayerConfig() { @@ -1307,15 +1135,20 @@ namespace Barotrauma gameplay.Add(jobPreferences); doc.Root.Add(gameplay); - var playerElement = new XElement("player", - new XAttribute("name", playerName ?? ""), - new XAttribute("headindex", CharacterHeadIndex), - new XAttribute("gender", CharacterGender), - new XAttribute("race", CharacterRace), - new XAttribute("hairindex", CharacterHairIndex), - new XAttribute("beardindex", CharacterBeardIndex), - new XAttribute("moustacheindex", CharacterMoustacheIndex), - new XAttribute("faceattachmentindex", CharacterFaceAttachmentIndex)); + var playerElement = new XElement("player", new XAttribute("name", playerName ?? "")); + if (PlayerCharacterCustomization != null) + { + playerElement.SetAttributeValue("headindex", PlayerCharacterCustomization.HeadSpriteId); + playerElement.SetAttributeValue("gender", PlayerCharacterCustomization.gender); + playerElement.SetAttributeValue("race", PlayerCharacterCustomization.race); + playerElement.SetAttributeValue("hairindex", PlayerCharacterCustomization.HairIndex); + playerElement.SetAttributeValue("beardindex", PlayerCharacterCustomization.BeardIndex); + playerElement.SetAttributeValue("moustacheindex", PlayerCharacterCustomization.MoustacheIndex); + playerElement.SetAttributeValue("faceattachmentindex", PlayerCharacterCustomization.FaceAttachmentIndex); + playerElement.SetAttributeValue("skincolor", XMLExtensions.ColorToString(PlayerCharacterCustomization.SkinColor)); + playerElement.SetAttributeValue("haircolor", XMLExtensions.ColorToString(PlayerCharacterCustomization.HairColor)); + playerElement.SetAttributeValue("facialhaircolor", XMLExtensions.ColorToString(PlayerCharacterCustomization.FacialHairColor)); + } doc.Root.Add(playerElement); #if CLIENT @@ -1434,23 +1267,22 @@ namespace Barotrauma if (playerElement != null) { playerName = playerElement.GetAttributeString("name", playerName); - CharacterHeadIndex = playerElement.GetAttributeInt("headindex", CharacterHeadIndex); - if (Enum.TryParse(playerElement.GetAttributeString("gender", "none"), true, out Gender g)) + 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); + int hair = playerElement.GetAttributeInt("hairindex", -1); + int beard = playerElement.GetAttributeInt("beardindex", -1); + int moustache = playerElement.GetAttributeInt("moustacheindex", -1); + int faceAttachment = playerElement.GetAttributeInt("faceattachmentindex", -1); + Color skinColor = playerElement.GetAttributeColor("skincolor", Color.Black); + Color hairColor = playerElement.GetAttributeColor("haircolor", Color.Black); + Color facialHairColor = playerElement.GetAttributeColor("facialhaircolor", Color.Black); + PlayerCharacterCustomization = new CharacterInfo.HeadInfo(head, gender, race, hair, beard, moustache, faceAttachment) { - CharacterGender = g; - } - if (Enum.TryParse(playerElement.GetAttributeString("race", "white"), true, out Race r)) - { - CharacterRace = r; - } - else - { - CharacterRace = Race.White; - } - CharacterHairIndex = playerElement.GetAttributeInt("hairindex", CharacterHairIndex); - CharacterBeardIndex = playerElement.GetAttributeInt("beardindex", CharacterBeardIndex); - CharacterMoustacheIndex = playerElement.GetAttributeInt("moustacheindex", CharacterMoustacheIndex); - CharacterFaceAttachmentIndex = playerElement.GetAttributeInt("faceattachmentindex", CharacterFaceAttachmentIndex); + SkinColor = skinColor, + HairColor = hairColor, + FacialHairColor = facialHairColor + }; } } @@ -1656,13 +1488,7 @@ namespace Barotrauma UseSteamMatchmaking = true; RequireSteamAuthentication = true; QuickStartSubmarineName = string.Empty; - CharacterHeadIndex = 1; - CharacterHairIndex = -1; - CharacterBeardIndex = -1; - CharacterMoustacheIndex = -1; - CharacterFaceAttachmentIndex = -1; - CharacterGender = Gender.None; - CharacterRace = Race.White; + PlayerCharacterCustomization = null; aimAssistAmount = 0.5f; EnableMouseLook = true; EnableRadialDistortion = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs index 3a1381b97..3f65446a4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs @@ -15,14 +15,14 @@ namespace Barotrauma.Items.Components private Character targetCharacter; private AfflictionPrefab selectedEffect, selectedTaintedEffect; - [Serialize("", false)] + [Serialize("", true)] public string Effect { get; set; } - [Serialize("geneticmaterialdebuff", false)] + [Serialize("geneticmaterialdebuff", true)] public string TaintedEffect { get; @@ -30,7 +30,7 @@ namespace Barotrauma.Items.Components } private bool tainted; - [Serialize(false, false)] + [Serialize(false, true)] public bool Tainted { get { return tainted; } @@ -49,7 +49,7 @@ namespace Barotrauma.Items.Components } //only for saving the selected tainted effect - [Serialize("", false)] + [Serialize("", true)] public string SelectedTaintedEffect { get { return selectedTaintedEffect?.Identifier ?? string.Empty; } @@ -100,6 +100,11 @@ namespace Barotrauma.Items.Components { float selectedTaintedEffectStrength = item.ConditionPercentage / 100.0f * selectedTaintedEffect.MaxStrength; character.CharacterHealth.ApplyAffliction(null, selectedTaintedEffect.Instantiate(selectedTaintedEffectStrength)); + var existingAffliction = character.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedTaintedEffect); + if (existingAffliction != null) + { + existingAffliction.Strength = selectedTaintedEffectStrength; + } targetCharacter = character; #if SERVER item.CreateServerEvent(this); @@ -111,6 +116,11 @@ namespace Barotrauma.Items.Components ApplyStatusEffects(ActionType.OnWearing, 1.0f); float selectedEffectStrength = item.ConditionPercentage / 100.0f * selectedEffect.MaxStrength; character.CharacterHealth.ApplyAffliction(null, selectedEffect.Instantiate(selectedEffectStrength)); + var existingAffliction = character.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedEffect); + if (existingAffliction != null) + { + existingAffliction.Strength = selectedEffectStrength; + } targetCharacter = character; #if SERVER item.CreateServerEvent(this); @@ -197,5 +207,21 @@ namespace Barotrauma.Items.Components item.CreateServerEvent(this); #endif } + + public static string TryCreateName(ItemPrefab prefab, XElement element) + { + foreach (XElement subElement in element.Elements()) + { + if (subElement.Name.ToString().Equals(nameof(GeneticMaterial), StringComparison.OrdinalIgnoreCase)) + { + string nameId = subElement.GetAttributeString("nameidentifier", ""); + if (!string.IsNullOrEmpty(nameId)) + { + return prefab.Name.Replace("[type]", TextManager.Get(nameId, returnNull: true) ?? nameId); + } + } + } + return prefab.Name; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index 8c48733a9..942985449 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -603,6 +603,11 @@ namespace Barotrauma.Items.Components return true; } + public override bool SecondaryUse(float deltaTime, Character character = null) + { + return true; + } + private Vector2 GetAttachPosition(Character user, bool useWorldCoordinates = false) { if (user == null) { return useWorldCoordinates ? item.WorldPosition : item.Position; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index 312df8e44..cb96a5935 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -61,8 +61,10 @@ namespace Barotrauma.Items.Components foreach (XElement subElement in element.Elements()) { if (!subElement.Name.ToString().Equals("attack", StringComparison.OrdinalIgnoreCase)) { continue; } - Attack = new Attack(subElement, item.Name + ", MeleeWeapon", item); - Attack.DamageRange = item.body == null ? 10.0f : ConvertUnits.ToDisplayUnits(item.body.GetMaxExtent()); + Attack = new Attack(subElement, item.Name + ", MeleeWeapon", item) + { + DamageRange = item.body == null ? 10.0f : ConvertUnits.ToDisplayUnits(item.body.GetMaxExtent()) + }; } item.IsShootable = true; // TODO: should define this in xml if we have melee weapons that don't require aim to use @@ -266,16 +268,10 @@ namespace Barotrauma.Items.Components return false; } - Character targetCharacter = null; - Limb targetLimb = null; - Structure targetStructure = null; - Item targetItem = null; - - if (f2.Body.UserData is Limb) + if (f2.Body.UserData is Limb targetLimb) { - targetLimb = (Limb)f2.Body.UserData; if (targetLimb.IsSevered || targetLimb.character == null || targetLimb.character == User) { return false; } - targetCharacter = targetLimb.character; + var targetCharacter = targetLimb.character; if (targetCharacter == picker) { return false; } if (AllowHitMultiple) { @@ -287,9 +283,8 @@ namespace Barotrauma.Items.Components } hitTargets.Add(targetCharacter); } - else if (f2.Body.UserData is Character) + else if (f2.Body.UserData is Character targetCharacter) { - targetCharacter = (Character)f2.Body.UserData; if (targetCharacter == picker || targetCharacter == User) { return false; } targetLimb = targetCharacter.AnimController.GetLimb(LimbType.Torso); //Otherwise armor can be bypassed in strange ways if (AllowHitMultiple) @@ -302,9 +297,8 @@ namespace Barotrauma.Items.Components } hitTargets.Add(targetCharacter); } - else if (f2.Body.UserData is Structure) + else if (f2.Body.UserData is Structure targetStructure) { - targetStructure = (Structure)f2.Body.UserData; if (AllowHitMultiple) { if (hitTargets.Contains(targetStructure)) { return true; } @@ -315,9 +309,8 @@ namespace Barotrauma.Items.Components } hitTargets.Add(targetStructure); } - else if (f2.Body.UserData is Item) + else if (f2.Body.UserData is Item targetItem) { - targetItem = (Item)f2.Body.UserData; if (AllowHitMultiple) { if (hitTargets.Contains(targetItem)) { return true; } @@ -350,13 +343,11 @@ namespace Barotrauma.Items.Components Limb targetLimb = target.UserData as Limb; Character targetCharacter = targetLimb?.character ?? target.UserData as Character; - Structure targetStructure = target.UserData as Structure; - Item targetItem = target.UserData as Item; - if (Attack != null) { Attack.SetUser(User); Attack.DamageMultiplier = 1 + User.GetStatValue(StatTypes.MeleeAttackMultiplier); + Attack.DamageMultiplier *= 1.0f + item.GetQualityModifier(Quality.StatType.AttackMultiplier); if (targetLimb != null) { @@ -370,12 +361,12 @@ namespace Barotrauma.Items.Components targetCharacter.LastDamageSource = item; Attack.DoDamage(User, targetCharacter, item.WorldPosition, 1.0f); } - else if (targetStructure != null) + else if (target.UserData is Structure targetStructure) { if (targetStructure.Removed) { return; } Attack.DoDamage(User, targetStructure, item.WorldPosition, 1.0f); } - else if (targetItem != null && targetItem.Prefab.DamagedByMeleeWeapons && targetItem.Condition > 0) + else if (target.UserData is Item targetItem && targetItem.Prefab.DamagedByMeleeWeapons && targetItem.Condition > 0) { if (targetItem.Removed) { return; } Attack.DoDamage(User, targetItem, item.WorldPosition, 1.0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs index 73d89fe0f..60c128e6a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs @@ -196,7 +196,8 @@ 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); - projectile.Shoot(character, character.AnimController.AimSourceSimPos, barrelPos, rotation + spread, ignoredBodies: limbBodies.ToList(), createNetworkEvent: false); + 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); if (i == 0) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index 1ed8b6edc..4aaceab07 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -619,7 +619,7 @@ namespace Barotrauma.Items.Components levelResource.requiredItems.Any() && levelResource.HasRequiredItems(user, addMessage: false)) { - float addedDetachTime = deltaTime * (1f + user.GetStatValue(StatTypes.RepairToolDeattachTimeMultiplier)) * item.GetQualityModifier(Quality.StatType.RepairToolDeattachTimeMultiplier); + float addedDetachTime = deltaTime * (1f + user.GetStatValue(StatTypes.RepairToolDeattachTimeMultiplier)) * (1f + item.GetQualityModifier(Quality.StatType.RepairToolDeattachTimeMultiplier)); levelResource.DeattachTimer += addedDetachTime; #if CLIENT Character.Controlled?.UpdateHUDProgressBar( diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs index 158af353e..e779c75ba 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs @@ -19,6 +19,8 @@ namespace Barotrauma.Items.Components private float userDeconstructorSpeedMultiplier = 1.0f; + private const float TinkeringSpeedIncrease = 1.5f; + private ItemContainer inputContainer, outputContainer; public ItemContainer InputContainer @@ -89,12 +91,20 @@ namespace Barotrauma.Items.Components if (powerConsumption <= 0.0f) { Voltage = 1.0f; } progressTimer += deltaTime * Math.Min(Voltage, 1.0f); + float tinkeringStrength = 0f; + if (repairable.IsTinkering) + { + tinkeringStrength = repairable.TinkeringStrength; + } + // doesn't quite work properly, remaining time changes if tinkering stops + float deconstructionSpeedModifier = userDeconstructorSpeedMultiplier * (1f + tinkeringStrength * TinkeringSpeedIncrease); + if (DeconstructItemsSimultaneously) { float deconstructTime = 0.0f; foreach (Item targetItem in inputContainer.Inventory.AllItems) { - deconstructTime += targetItem.Prefab.DeconstructTime / (DeconstructionSpeed * userDeconstructorSpeedMultiplier); + deconstructTime += targetItem.Prefab.DeconstructTime / (DeconstructionSpeed * deconstructionSpeedModifier); } progressState = Math.Min(progressTimer / deconstructTime, 1.0f); @@ -126,7 +136,7 @@ namespace Barotrauma.Items.Components var validDeconstructItems = targetItem.Prefab.DeconstructItems.FindAll(it => it.RequiredDeconstructor.Length == 0 || it.RequiredDeconstructor.Any(r => item.HasTag(r) || item.Prefab.Identifier.Equals(r, StringComparison.OrdinalIgnoreCase))); - float deconstructTime = validDeconstructItems.Any() ? targetItem.Prefab.DeconstructTime / DeconstructionSpeed : 1.0f; + float deconstructTime = validDeconstructItems.Any() ? targetItem.Prefab.DeconstructTime / (DeconstructionSpeed * deconstructionSpeedModifier) : 1.0f; progressState = Math.Min(progressTimer / deconstructTime, 1.0f); if (progressTimer > deconstructTime) @@ -234,9 +244,13 @@ namespace Barotrauma.Items.Components if (user != null && !user.Removed) { - var itemsCreated = new AbilityValueItem(1f, targetItem.Prefab); + var itemsCreated = new AbilityValueItem(amount, targetItem.Prefab); user.CheckTalents(AbilityEffectType.OnItemDeconstructedMaterial, itemsCreated); amount = (int)itemsCreated.Value; + + // used to spawn items directly into the deconstructor + var itemContainer = new AbilityItemPrefabItem(item, targetItem.Prefab); + user.CheckTalents(AbilityEffectType.OnItemDeconstructedInventory, itemContainer); } for (int i = 0; i < amount; i++) @@ -256,17 +270,6 @@ namespace Barotrauma.Items.Components } } - if (user != null && !user.Removed) - { - var deconstructItemRetainProbability = new AbilityValueItem(0f, targetItem.Prefab); - user.CheckTalents(AbilityEffectType.OnItemDeconstructedRetainProbability, deconstructItemRetainProbability); - - if (deconstructItemRetainProbability.Value > Rand.Range(0f, 1f, Rand.RandSync.Unsynced)) - { - allowRemove = false; - } - } - if (targetItem.AllowDeconstruct && allowRemove) { //drop all items that are inside the deconstructed item diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs index e5849b584..266c28fff 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs @@ -73,6 +73,8 @@ namespace Barotrauma.Items.Components } } + private const float TinkeringForceIncrease = 1.5f; + public Engine(Item item, XElement element) : base(item, element) { @@ -128,7 +130,7 @@ namespace Barotrauma.Items.Components currForce *= maxForce * forceMultiplier; if (item.GetComponent() is Repairable repairable && repairable.IsTinkering) { - currForce *= 2.5f; + currForce *= 1f + repairable.TinkeringStrength * TinkeringForceIncrease; } //less effective when in a bad condition diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index f5cb14ba8..43cba9600 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -32,6 +32,8 @@ namespace Barotrauma.Items.Components [Serialize(1.0f, true)] public float SkillRequirementMultiplier { get; set; } + private const float TinkeringSpeedIncrease = 1.5f; + private enum FabricatorState { Active = 1, @@ -279,7 +281,14 @@ namespace Barotrauma.Items.Components if (powerConsumption <= 0) { Voltage = 1.0f; } - timeUntilReady -= deltaTime * Math.Min(Voltage, 1.0f); + float tinkeringStrength = 0f; + if (repairable.IsTinkering) + { + tinkeringStrength = repairable.TinkeringStrength; + } + float fabricationSpeedIncrease = 1f + tinkeringStrength * TinkeringSpeedIncrease; + + timeUntilReady -= deltaTime * fabricationSpeedIncrease * Math.Min(Voltage, 1.0f); UpdateRequiredTimeProjSpecific(); @@ -329,12 +338,7 @@ namespace Barotrauma.Items.Components } user.CheckTalents(AbilityEffectType.OnItemFabricatedAmount, fabricationValueItem); - float floatQuality = 0.0f; - foreach (string tag in fabricatedItem.TargetItem.Tags) - { - floatQuality += user.Info.GetSavedStatValue(StatTypes.IncreaseFabricationQuality, tag); - } - quality = (int)floatQuality; + quality = GetFabricatedItemQuality(fabricatedItem, user); } var tempUser = user; @@ -404,6 +408,25 @@ namespace Barotrauma.Items.Components } } + private int GetFabricatedItemQuality(FabricationRecipe fabricatedItem, Character user) + { + if (user == null) { return 0; } + if (fabricatedItem.TargetItem.ConfigElement.GetChildElement("Quality") == null) { return 0; } + int quality = 0; + float floatQuality = 0.0f; + foreach (string tag in fabricatedItem.TargetItem.Tags) + { + floatQuality += user.Info.GetSavedStatValue(StatTypes.IncreaseFabricationQuality, tag); + } + quality = (int)floatQuality; + + const int MaxCraftingSkill = 100; + + quality += fabricatedItem.RequiredSkills.All(s => user.GetSkillLevel(s.Identifier) >= MaxCraftingSkill) ? 1 : 0; + quality += FabricationDegreeOfSuccess(user, fabricatedItem.RequiredSkills) >= 0.5f ? 1 : 0; + return quality; + } + partial void UpdateRequiredTimeProjSpecific(); private bool CanBeFabricated(FabricationRecipe fabricableItem, Dictionary> availableIngredients, Character character) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs index bacb23c6e..351e57ef4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs @@ -70,6 +70,8 @@ namespace Barotrauma.Items.Components public bool HasPower => IsActive && Voltage >= MinVoltage; public bool IsAutoControlled => pumpSpeedLockTimer > 0.0f || isActiveLockTimer > 0.0f; + private const float TinkeringSpeedIncrease = 1.5f; + public Pump(Item item, XElement element) : base(item, element) { @@ -108,7 +110,7 @@ namespace Barotrauma.Items.Components if (item.GetComponent() is Repairable repairable && repairable.IsTinkering) { - currFlow *= 2.5f; + currFlow *= 1f + repairable.TinkeringStrength * TinkeringSpeedIncrease; } //less effective when in a bad condition diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs index 5c8e7e70e..0fdba45ca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs @@ -370,7 +370,7 @@ namespace Barotrauma.Items.Components item.SendSignal(new Signal((ConvertUnits.ToDisplayUnits(sub.Velocity.X * Physics.DisplayToRealWorldRatio) * 3.6f).ToString("0.0000", CultureInfo.InvariantCulture), sender: user), "current_velocity_x"); item.SendSignal(new Signal((ConvertUnits.ToDisplayUnits(sub.Velocity.Y * Physics.DisplayToRealWorldRatio) * -3.6f).ToString("0.0000", CultureInfo.InvariantCulture), sender: user), "current_velocity_y"); - item.SendSignal(new Signal(sub.WorldPosition.X.ToString("0.0000", CultureInfo.InvariantCulture), sender: user), "current_position_x"); + item.SendSignal(new Signal((sub.WorldPosition.X * Physics.DisplayToRealWorldRatio).ToString("0.0000", CultureInfo.InvariantCulture), sender: user), "current_position_x"); item.SendSignal(new Signal(sub.RealWorldDepth.ToString("0.0000", CultureInfo.InvariantCulture), sender: user), "current_position_y"); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index 6af8ff629..07f99e1ea 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -227,10 +227,11 @@ namespace Barotrauma.Items.Components } } - private void Launch(Character user, Vector2 simPosition, float rotation) + private void Launch(Character user, Vector2 simPosition, float rotation, float damageMultiplier = 1f) { Item.body.ResetDynamics(); Item.SetTransform(simPosition, rotation); + Attack.DamageMultiplier = damageMultiplier; // Set user for hitscan projectiles to work properly. User = user; // Need to set null for non-characterusable items. @@ -243,7 +244,7 @@ namespace Barotrauma.Items.Components Item.SetTransform(simPosition, rotation + (Item.body.Dir * LaunchRotationRadians)); } - public void Shoot(Character user, Vector2 weaponPos, Vector2 spawnPos, float rotation, List ignoredBodies, bool createNetworkEvent) + public void Shoot(Character user, Vector2 weaponPos, Vector2 spawnPos, float rotation, List ignoredBodies, bool createNetworkEvent, float damageMultiplier = 1f) { //add the limbs of the shooter to the list of bodies to be ignored //so that the player can't shoot himself @@ -264,7 +265,7 @@ namespace Barotrauma.Items.Components projectilePos = newPos; } } - Launch(user, projectilePos, rotation); + Launch(user, projectilePos, rotation, damageMultiplier); if (createNetworkEvent && !Item.Removed && GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { #if SERVER diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs index 5fbb40733..875942fca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs @@ -114,6 +114,9 @@ namespace Barotrauma.Items.Components private Item currentRepairItem; private float tinkeringDuration; + private float tinkeringStrength; + + public float TinkeringStrength => tinkeringStrength; public enum FixActions : int { @@ -240,6 +243,8 @@ namespace Barotrauma.Items.Components CurrentFixerAction = action; if (action == FixActions.Tinker) { + tinkeringStrength = 1f + CurrentFixer.GetStatValue(StatTypes.TinkeringStrength); + if (character.HasAbilityFlag(AbilityFlags.CanTinkerFabricatorsAndDeconstructors) && item.GetComponent() != null || item.GetComponent() != null) { // fabricators and deconstructors can be tinkered indefinitely (more or less) @@ -370,6 +375,10 @@ namespace Barotrauma.Items.Components { tinkeringDuration -= deltaTime; // not great to interject it here, should be less reliant on returning + + float conditionDecrease = deltaTime * (CurrentFixer.GetStatValue(StatTypes.TinkeringDamage) / item.MaxCondition) * 100f; + item.Condition -= conditionDecrease; + if (!CanTinker(CurrentFixer) || tinkeringDuration <= 0f) { StopRepairing(CurrentFixer); @@ -476,8 +485,8 @@ namespace Barotrauma.Items.Components { if (!character.HasAbilityFlag(AbilityFlags.CanTinker)) { return false; } if (item.GetComponent() != null) { return true; } - if (item.GetComponent() != null) { return true; } if (item.GetComponent() != null) { return true; } + if (item.HasTag("turretammosource")) { return true; } if (!character.HasAbilityFlag(AbilityFlags.CanTinkerFabricatorsAndDeconstructors)) { return false; } if (item.GetComponent() != null) { return true; } if (item.GetComponent() != null) { return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs index ed67a0e46..d19349597 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs @@ -80,6 +80,7 @@ namespace Barotrauma.Items.Components public RegExFindComponent(Item item, XElement element) : base(item, element) { + nonContinuousOutputSent = true; IsActive = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index 759893105..39d5a30ae 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -64,7 +64,9 @@ namespace Barotrauma.Items.Components private Character currentTarget; const float aiFindTargetInterval = 5.0f; - private const float TinkeringPowerCostReduction = 1.25f; + private const float TinkeringPowerCostReduction = 0.2f; + private const float TinkeringDamageIncrease = 0.2f; + private const float TinkeringReloadDecrease = 0.2f; public float Rotation { @@ -560,7 +562,8 @@ namespace Barotrauma.Items.Components Projectile launchedProjectile = null; bool loaderBroken = false; - bool isTinkering = false; + float tinkeringStrength = 0f; + for (int i = 0; i < ProjectileCount; i++) { var projectiles = GetLoadedProjectiles(); @@ -624,9 +627,9 @@ namespace Barotrauma.Items.Components { if (!(e is Item linkedItem)) { continue; } if (!item.prefab.IsLinkAllowed(e.prefab)) { continue; } - if (linkedItem.GetComponent() is Repairable repairable && linkedItem.HasTag("turretammosource")) + if (linkedItem.GetComponent() is Repairable repairable && repairable.IsTinkering && linkedItem.HasTag("turretammosource")) { - isTinkering = repairable.IsTinkering; + tinkeringStrength = repairable.TinkeringStrength; } } @@ -636,10 +639,8 @@ namespace Barotrauma.Items.Components float neededPower = GetPowerRequiredToShoot(); // tinkering is currently not factored into the common method as it is checked only when shooting // but this is a minor issue that causes mostly cosmetic woes. might still be worth refactoring later - if (isTinkering) - { - neededPower /= TinkeringPowerCostReduction; - } + neededPower /= 1f + (tinkeringStrength * TinkeringPowerCostReduction); + while (neededPower > 0.0001f && batteries.Count > 0) { batteries.RemoveAll(b => b.Charge <= 0.0001f || b.MaxOutPut <= 0.0001f); @@ -673,12 +674,12 @@ namespace Barotrauma.Items.Components { foreach (Projectile projectile in projectiles) { - Launch(projectile.Item, character, isTinkering: isTinkering); + Launch(projectile.Item, character, tinkeringStrength: tinkeringStrength); } } else { - Launch(null, character, isTinkering: isTinkering); + Launch(null, character, tinkeringStrength: tinkeringStrength); } if (item.AiTarget != null) { @@ -712,13 +713,10 @@ namespace Barotrauma.Items.Components return true; } - private void Launch(Item projectile, Character user = null, float? launchRotation = null, bool isTinkering = false) + private void Launch(Item projectile, Character user = null, float? launchRotation = null, float tinkeringStrength = 0f) { reload = reloadTime; - if (isTinkering) - { - reload /= 1.25f; - } + reload /= 1f + (tinkeringStrength * TinkeringReloadDecrease); if (user != null) { @@ -747,10 +745,8 @@ namespace Barotrauma.Items.Components if (projectileComponent != null) { projectileComponent.Attacker = projectileComponent.User = user; - if (isTinkering) - { - projectileComponent.Attack.DamageMultiplier = 1.25f; - } + projectileComponent.Attack.DamageMultiplier = 1f + (TinkeringDamageIncrease * tinkeringStrength); + projectileComponent.Use(); projectile.GetComponent()?.Attach(item, projectile); projectileComponent.User = user; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs index 5c3ccc0b8..62995f7c4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs @@ -5,7 +5,6 @@ using Barotrauma.IO; using System.Linq; using System.Xml.Linq; using Barotrauma.Items.Components; -using Barotrauma.Extensions; using Barotrauma.Networking; using Barotrauma.Abilities; @@ -49,7 +48,13 @@ namespace Barotrauma public bool HideOtherWearables { get; private set; } public List HideWearablesOfType { get; private set; } public bool InheritLimbDepth { get; private set; } - public bool InheritTextureScale { get; private set; } + /// + /// Does the wearable inherit all the scalings of the wearer? Also the wearable's own scale is used! + /// + public bool InheritScale { get; private set; } + public bool IgnoreRagdollScale { get; private set; } + public bool IgnoreLimbScale { get; private set; } + public bool IgnoreTextureScale { get; private set; } public bool InheritOrigin { get; private set; } public bool InheritSourceRect { get; private set; } @@ -113,10 +118,9 @@ namespace Barotrauma case WearableType.Husk: case WearableType.Herpes: Limb = LimbType.Head; - HideLimb = type == WearableType.Husk || type == WearableType.Herpes; HideOtherWearables = false; InheritLimbDepth = true; - InheritTextureScale = true; + InheritScale = true; InheritOrigin = true; InheritSourceRect = true; break; @@ -173,7 +177,19 @@ namespace Barotrauma HideLimb = SourceElement.GetAttributeBool("hidelimb", false); HideOtherWearables = SourceElement.GetAttributeBool("hideotherwearables", false); InheritLimbDepth = SourceElement.GetAttributeBool("inheritlimbdepth", true); - InheritTextureScale = SourceElement.GetAttributeBool("inherittexturescale", false); + var scale = SourceElement.GetAttribute("inheritscale"); + if (scale != null) + { + InheritScale = scale.GetAttributeBool(false); + } + else + { + InheritScale = SourceElement.GetAttributeBool("inherittexturescale", false); + } + IgnoreLimbScale = SourceElement.GetAttributeBool("ignorelimbscale", false); + IgnoreTextureScale = SourceElement.GetAttributeBool("ignoretexturescale", false); + IgnoreRagdollScale = SourceElement.GetAttributeBool("ignoreragdollscale", false); + SourceElement.GetAttributeBool("inherittexturescale", false); InheritOrigin = SourceElement.GetAttributeBool("inheritorigin", false); InheritSourceRect = SourceElement.GetAttributeBool("inheritsourcerect", false); DepthLimb = (LimbType)Enum.Parse(typeof(LimbType), SourceElement.GetAttributeString("depthlimb", "None"), true); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index 54f1daca4..ded5ee66a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -842,6 +842,8 @@ namespace Barotrauma } } + name = GeneticMaterial.TryCreateName(this, element); + if (string.IsNullOrEmpty(name)) { DebugConsole.ThrowError($"Unnamed item ({identifier}) in {filePath}!"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs index 2efb1e919..c2dd49b6c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs @@ -167,7 +167,8 @@ namespace Barotrauma new XAttribute("type", type.ToString()), new XAttribute("optional", IsOptional), new XAttribute("ignoreineditor", IgnoreInEditor), - new XAttribute("excludebroken", ExcludeBroken)); + new XAttribute("excludebroken", ExcludeBroken), + new XAttribute("targetslot", TargetSlot)); if (excludedIdentifiers.Length > 0) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index 3d0f8ee77..277b55925 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -340,7 +340,7 @@ namespace Barotrauma //ensures that the attack hits the correct limb and that the direction of the hit can be determined correctly in the AddDamage methods Vector2 dir = worldPosition - limb.WorldPosition; Vector2 hitPos = limb.WorldPosition + (dir.LengthSquared() <= 0.001f ? Rand.Vector(1.0f) : Vector2.Normalize(dir)) * 0.01f; - AttackResult attackResult = c.AddDamage(hitPos, modifiedAfflictions, attack.Stun * distFactor, false, attacker: attacker); + AttackResult attackResult = c.AddDamage(hitPos, modifiedAfflictions, attack.Stun * distFactor, false, attacker: attacker, damageMultiplier: attack.DamageMultiplier); damages.Add(limb, attackResult.Damage); if (attack.StatusEffects != null && attack.StatusEffects.Any()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index e32aa7ed7..5fd474745 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -2563,10 +2563,18 @@ namespace Barotrauma if (PositionsOfInterest.Any(p => p.PositionType == PositionType.Cave)) { positionType = PositionType.Cave; + if (allValidLocations.Any(l => l.Edge.NextToCave)) + { + allValidLocations.RemoveAll(l => !l.Edge.NextToCave); + } } else if (PositionsOfInterest.Any(p => p.PositionType == PositionType.SidePath)) { positionType = PositionType.SidePath; + if (allValidLocations.Any(l => l.Edge.NextToSidePath)) + { + allValidLocations.RemoveAll(l => !l.Edge.NextToSidePath); + } } var poi = PositionsOfInterest.GetRandom(p => p.PositionType == positionType, randSync: Rand.RandSync.Server); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index 153bfa954..4fedac730 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -60,7 +60,7 @@ namespace Barotrauma private LocationType addInitialMissionsForType; - public bool Discovered; + public bool Discovered { get; private set; } public readonly Dictionary ProximityTimer = new Dictionary(); public (LocationTypeChange typeChange, int delay, MissionPrefab parentMission)? PendingLocationTypeChange; @@ -868,6 +868,8 @@ namespace Barotrauma // Adjust by random price modifier price = ((100 + StorePriceModifier) / 100.0f) * price; + price *= priceInfo.BuyingPriceMultiplier; + // Adjust by daily special status if (considerDailySpecials && DailySpecials.Contains(item)) { @@ -1111,6 +1113,30 @@ namespace Barotrauma return nextStatus; } + public void Discover(bool checkTalents = true) + { + if (Discovered) { return; } + Discovered = true; + if (checkTalents) + { + GameSession.GetSessionCrewCharacters().ForEach(c => c.CheckTalents(AbilityEffectType.OnLocationDiscovered, new Abilities.AbilityLocation(this))); + } + } + + public void Reset() + { + if (Type != OriginalType) + { + ChangeType(OriginalType); + PendingLocationTypeChange = null; + } + CreateStore(force: true); + ClearMissions(); + LevelData?.EventHistory?.Clear(); + UnlockInitialMissions(); + Discovered = false; + } + public XElement Save(Map map, XElement parentElement) { var locationElement = new XElement("location", diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index e49683e88..76b284bdc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -231,7 +231,7 @@ namespace Barotrauma } System.Diagnostics.Debug.Assert(StartLocation != null, "Start location not assigned after level generation."); - CurrentLocation.Discovered = true; + CurrentLocation.Discover(true); CurrentLocation.CreateStore(); InitProjectSpecific(); @@ -671,7 +671,7 @@ namespace Barotrauma SelectedConnection.Passed = true; CurrentLocation = SelectedLocation; - CurrentLocation.Discovered = true; + CurrentLocation.Discover(); SelectedLocation = null; CurrentLocation.CreateStore(); @@ -702,7 +702,7 @@ namespace Barotrauma Location prevLocation = CurrentLocation; CurrentLocation = Locations[index]; - CurrentLocation.Discovered = true; + CurrentLocation.Discover(); if (prevLocation != CurrentLocation) { @@ -1055,7 +1055,10 @@ namespace Barotrauma } } location.LoadLocationTypeChange(subElement); - location.Discovered = subElement.GetAttributeBool("discovered", false); + if (subElement.GetAttributeBool("discovered", false)) + { + location.Discover(checkTalents: false); + } if (location.Discovered) { #if CLIENT diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs index 78a7e14f3..25dff440e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs @@ -24,6 +24,11 @@ namespace Barotrauma /// The item isn't available in stores unless the level's difficulty is above this value /// public readonly int MinLevelDifficulty; + /// + /// The cost of item when sold by the store. Higher modifier means the item costs more to buy from the store. + /// + public readonly float BuyingPriceMultiplier = 1f; + /// /// Support for the old style of determining item prices @@ -34,6 +39,7 @@ namespace Barotrauma { Price = element.GetAttributeInt("buyprice", 0); MinLevelDifficulty = element.GetAttributeInt("minleveldifficulty", 0); + BuyingPriceMultiplier = element.GetAttributeFloat("buyingpricemultiplier", 1f); CanBeBought = true; var minAmount = GetMinAmount(element); MinAvailableAmount = Math.Min(minAmount, CargoManager.MaxQuantity); @@ -42,11 +48,12 @@ namespace Barotrauma MaxAvailableAmount = Math.Max(maxAmount, MinAvailableAmount); } - public PriceInfo(int price, bool canBeBought, int minAmount = 0, int maxAmount = 0, bool canBeSpecial = true, int minLevelDifficulty = 0) + public PriceInfo(int price, bool canBeBought, int minAmount = 0, int maxAmount = 0, bool canBeSpecial = true, int minLevelDifficulty = 0, float buyingPriceMultiplier = 1f) { Price = price; CanBeBought = canBeBought; MinAvailableAmount = Math.Min(minAmount, CargoManager.MaxQuantity); + BuyingPriceMultiplier = buyingPriceMultiplier; maxAmount = Math.Min(maxAmount, CargoManager.MaxQuantity); MaxAvailableAmount = Math.Max(maxAmount, minAmount); MinLevelDifficulty = minLevelDifficulty; @@ -62,6 +69,7 @@ namespace Barotrauma var maxAmount = GetMaxAmount(element); var minLevelDifficulty = element.GetAttributeInt("minleveldifficulty", 0); var canBeSpecial = element.GetAttributeBool("canbespecial", true); + var buyingPriceMultiplier = element.GetAttributeFloat("buyingpricemultiplier", 1f); var priceInfos = new List>(); foreach (XElement childElement in element.GetChildElements("price")) @@ -73,7 +81,7 @@ namespace Barotrauma minAmount: sold ? GetMinAmount(childElement, minAmount) : 0, maxAmount: sold ? GetMaxAmount(childElement, maxAmount) : 0, canBeSpecial, - childElement.GetAttributeInt("minleveldifficulty", minLevelDifficulty)))); + childElement.GetAttributeInt("minleveldifficulty", minLevelDifficulty), childElement.GetAttributeFloat("buyingpricemultiplier", buyingPriceMultiplier)))); } var canBeBoughtAtOtherLocations = soldByDefault && element.GetAttributeBool("soldeverywhere", true); @@ -81,7 +89,7 @@ namespace Barotrauma minAmount: canBeBoughtAtOtherLocations ? minAmount : 0, maxAmount: canBeBoughtAtOtherLocations ? maxAmount : 0, canBeSpecial, - minLevelDifficulty); + minLevelDifficulty, buyingPriceMultiplier); return priceInfos; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IReadMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IReadMessage.cs index 8a6d8f9e4..f7e5e730c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IReadMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IReadMessage.cs @@ -19,6 +19,8 @@ namespace Barotrauma.Networking Double ReadDouble(); UInt32 ReadVariableUInt32(); String ReadString(); + Microsoft.Xna.Framework.Color ReadColorR8G8B8(); + Microsoft.Xna.Framework.Color ReadColorR8G8B8A8(); int ReadRangedInteger(int min, int max); Single ReadRangedSingle(Single min, Single max, int bitCount); byte[] ReadBytes(int numberOfBytes); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IWriteMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IWriteMessage.cs index 653364ae1..16146f8bf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IWriteMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/IWriteMessage.cs @@ -15,6 +15,8 @@ namespace Barotrauma.Networking void Write(UInt64 val); void Write(Single val); void Write(Double val); + void WriteColorR8G8B8(Microsoft.Xna.Framework.Color val); + void WriteColorR8G8B8A8(Microsoft.Xna.Framework.Color val); void WriteVariableUInt32(UInt32 val); void Write(string val); void WriteRangedInteger(int val, int min, int max); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs index a84520c56..069c59aed 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs @@ -5,6 +5,7 @@ using Barotrauma.IO; using System.IO.Compression; using System.Runtime.InteropServices; using System.Text; +using Microsoft.Xna.Framework; namespace Barotrauma.Networking { @@ -138,6 +139,26 @@ namespace Barotrauma.Networking byte[] bytes = BitConverter.GetBytes(val); WriteBytes(ref buf, ref bitPos, bytes, 0, 8); } + + internal static void WriteColorR8G8B8(ref byte[] buf, ref int bitPos, Microsoft.Xna.Framework.Color val) + { + EnsureBufferSize(ref buf, bitPos + 24); + + Write(ref buf, ref bitPos, val.R); + Write(ref buf, ref bitPos, val.G); + Write(ref buf, ref bitPos, val.B); + } + + internal static void WriteColorR8G8B8A8(ref byte[] buf, ref int bitPos, Microsoft.Xna.Framework.Color val) + { + EnsureBufferSize(ref buf, bitPos + 32); + + Write(ref buf, ref bitPos, val.R); + Write(ref buf, ref bitPos, val.G); + Write(ref buf, ref bitPos, val.B); + Write(ref buf, ref bitPos, val.A); + } + internal static void Write(ref byte[] buf, ref int bitPos, string val) { if (string.IsNullOrEmpty(val)) @@ -299,6 +320,23 @@ namespace Barotrauma.Networking return BitConverter.ToDouble(bytes, 0); } + internal static Microsoft.Xna.Framework.Color ReadColorR8G8B8(byte[] buf, ref int bitPos) + { + byte r = ReadByte(buf, ref bitPos); + byte g = ReadByte(buf, ref bitPos); + byte b = ReadByte(buf, ref bitPos); + return new Color(r, g, b, (byte)255); + } + + internal static Microsoft.Xna.Framework.Color ReadColorR8G8B8A8(byte[] buf, ref int bitPos) + { + byte r = ReadByte(buf, ref bitPos); + byte g = ReadByte(buf, ref bitPos); + byte b = ReadByte(buf, ref bitPos); + byte a = ReadByte(buf, ref bitPos); + return new Color(r, g, b, a); + } + internal static UInt32 ReadVariableUInt32(byte[] buf, ref int bitPos) { int bitLength = buf.Length * 8; @@ -482,6 +520,16 @@ namespace Barotrauma.Networking MsgWriter.Write(ref buf, ref seekPos, val); } + public void WriteColorR8G8B8(Color val) + { + MsgWriter.WriteColorR8G8B8(ref buf, ref seekPos, val); + } + + public void WriteColorR8G8B8A8(Color val) + { + MsgWriter.WriteColorR8G8B8A8(ref buf, ref seekPos, val); + } + public void WriteVariableUInt32(UInt32 val) { MsgWriter.WriteVariableUInt32(ref buf, ref seekPos, val); @@ -702,6 +750,17 @@ namespace Barotrauma.Networking return MsgReader.ReadString(buf, ref seekPos); } + public Color ReadColorR8G8B8() + { + return MsgReader.ReadColorR8G8B8(buf, ref seekPos); + } + + public Color ReadColorR8G8B8A8() + { + return MsgReader.ReadColorR8G8B8A8(buf, ref seekPos); + } + + public int ReadRangedInteger(int min, int max) { return MsgReader.ReadRangedInteger(buf, ref seekPos, min, max); @@ -845,6 +904,16 @@ namespace Barotrauma.Networking MsgWriter.Write(ref buf, ref seekPos, val); } + public void WriteColorR8G8B8(Color val) + { + MsgWriter.WriteColorR8G8B8(ref buf, ref seekPos, val); + } + + public void WriteColorR8G8B8A8(Color val) + { + MsgWriter.WriteColorR8G8B8A8(ref buf, ref seekPos, val); + } + public void WriteVariableUInt32(UInt32 val) { MsgWriter.WriteVariableUInt32(ref buf, ref seekPos, val); @@ -936,6 +1005,16 @@ namespace Barotrauma.Networking return MsgReader.ReadString(buf, ref seekPos); } + public Color ReadColorR8G8B8() + { + return MsgReader.ReadColorR8G8B8(buf, ref seekPos); + } + + public Color ReadColorR8G8B8A8() + { + return MsgReader.ReadColorR8G8B8A8(buf, ref seekPos); + } + public int ReadRangedInteger(int min, int max) { return MsgReader.ReadRangedInteger(buf, ref seekPos, min, max); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs index 5002ddf18..c3e93bdd9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs @@ -88,7 +88,7 @@ namespace Barotrauma return GameMain.NetworkMember?.ServerSettings?.AllowLinkingWifiToChat ?? true; case ConditionType.IsSwappableItem: { - return entity is Item item && item.Prefab.SwappableItem != null; + return entity is Item item && item.Prefab.SwappableItem != null && Screen.Selected == GameMain.SubEditorScreen; } case ConditionType.AllowRotating: { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs b/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs index 6c0eccc0e..40d2a1742 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs @@ -236,7 +236,7 @@ namespace Barotrauma { lock (list) { - list.RemoveAll(wRef => !wRef.TryGetTarget(out Sprite s) || s==this); + list.RemoveAll(wRef => !wRef.TryGetTarget(out Sprite s) || s == this); } DisposeTexture(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs b/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs index 06710d345..c19d2db20 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs @@ -457,7 +457,14 @@ namespace Barotrauma */ - string extraDescriptionLine = Get(descriptionElement.GetAttributeString("tag", string.Empty)); + if (descriptionElement.GetAttributeBool("linebreak", false)) + { + Description += "\n"; + return; + } + + string descriptionTag = descriptionElement.GetAttributeString("tag", string.Empty); + string extraDescriptionLine = Get(descriptionTag); if (string.IsNullOrEmpty(extraDescriptionLine)) { return; } foreach (XElement replaceElement in descriptionElement.Elements()) { @@ -468,12 +475,23 @@ namespace Barotrauma string replacementValue = string.Empty; for (int i = 0; i < replacementValues.Length; i++) { +#if DEBUG + if (!int.TryParse(replacementValues[i], out int _) && !float.TryParse(replacementValues[i], System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out float __) && !ContainsTag(replacementValues[i])) + { + DebugConsole.AddWarning($"Couldn't find the tag \"{replacementValues[i]}\" in text files for description \"{descriptionTag}\". Is the tag correct?"); + } +#endif replacementValue += Get(replacementValues[i], returnNull: true) ?? replacementValues[i]; if (i < replacementValues.Length - 1) { replacementValue += ", "; } } + if (replaceElement.Attribute("color") != null) + { + string colorStr = replaceElement.GetAttributeString("color", "255,255,255,255"); + replacementValue = $"‖color:{colorStr}‖{replacementValue}‖color:end‖"; + } extraDescriptionLine = extraDescriptionLine.Replace(tag, replacementValue); } if (!string.IsNullOrEmpty(Description)) { Description += "\n"; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs index aef87c736..0621dd858 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs @@ -23,17 +23,19 @@ namespace Barotrauma.IO { path = System.IO.Path.GetFullPath(path).CleanUpPath(); - string extension = System.IO.Path.GetExtension(path).Replace(" ", ""); - if (unwritableExtensions.Any(e => e.Equals(extension, StringComparison.OrdinalIgnoreCase))) + if (!isDirectory) { - return false; - } - - if (!path.StartsWith(System.IO.Path.GetFullPath("Mods/").CleanUpPath(), StringComparison.OrdinalIgnoreCase) - && (extension.Equals(".dll", StringComparison.OrdinalIgnoreCase) - || extension.Equals(".exe", StringComparison.OrdinalIgnoreCase))) - { - return false; + string extension = System.IO.Path.GetExtension(path).Replace(" ", ""); + if (unwritableExtensions.Any(e => e.Equals(extension, StringComparison.OrdinalIgnoreCase))) + { + return false; + } + if (!path.StartsWith(System.IO.Path.GetFullPath("Mods/").CleanUpPath(), StringComparison.OrdinalIgnoreCase) + && (extension.Equals(".dll", StringComparison.OrdinalIgnoreCase) + || extension.Equals(".exe", StringComparison.OrdinalIgnoreCase))) + { + return false; + } } foreach (string unwritableDir in unwritableDirs) @@ -251,6 +253,7 @@ namespace Barotrauma.IO if (!Validation.CanWrite(path, true)) { DebugConsole.ThrowError($"Cannot create directory \"{path}\": modifying the contents of this folder/using this extension is not allowed."); + Validation.CanWrite(path, true); return null; } return System.IO.Directory.CreateDirectory(path); diff --git a/Barotrauma/BarotraumaShared/TintTest.png b/Barotrauma/BarotraumaShared/TintTest.png new file mode 100644 index 0000000000000000000000000000000000000000..77afc5ae9fb6bff9e517e2023347c9a5ec513924 GIT binary patch literal 91346 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 LNkvXXu0mjfS26k` literal 0 HcmV?d00001 diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 9a8ee1ea6..b84699444 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,38 @@ +--------------------------------------------------------------------------------------------------------- +v0.1500.4.0 +--------------------------------------------------------------------------------------------------------- + +- Overhauled character sprites, ragdolls and animations (WIP). +- Option to customize the starting crew in the single player campaign. +- Talent improvements and additions. +- Merged the talent and character tabs in the tab menu. +- Disable deconstructor button when there's no deconstructable items in the input slots (also applies to research terminals which are technically deconstructors). + +Fixes: +- Fixed inability to install/update mods that have periods in the name. +- Fixed stack sizes being displayed incorrectly on items with multiple inventories, e.g. deconstructor (unstable only). +- Fixed depleted fuel not being craftable (unstable only). +- Fixed leftmost inventory slot overlapping with the chatbox (unstable only). +- Fixed medic bots trying to treat genetic afflictions (unstable only). +- Fixed nav terminals "current_position_x" output being in pixels when "current_position_y" is in meters. +- Fixed tall subs overlapping with the buttons on the status monitor (unstable only). +- Fixed status monitor's item finder not finding wires (unstable only). +- Cargo scooters can't be put in toolbelts, crates, bandoliers or each other (unstable only). +- Fixed status monitor elements getting misaligned when 1st viewing it while linked to another interface and then individually or vice versa (unstable only). +- Fixed minerals sometimes spawning in unreachable spots in mining missions (on cells that are next to a cave, but at the wrong side of that cell if there's empty space behind it). +- Fixed items' "allow swapping" property being editable in-game. +- Fixed tainted genetic materials becoming untainted when saving and loading (unstable only). +- Fixed genetic material effects' strengths changing when saving and reloading (unstable only). +- Fixed tainted genetic materials sometimes giving the user hammerhead matriarch's genetic effects. +- Fixed inability to tinker loaders (unstable only). +- Fixed RegEx components with a non-continuous output always sending a signal out after being loaded. +- Fixed pirate subs sometimes spawning inside floating ice chunks. +- Fixed recommended treatments not changing when the strengths of the displayed afflictions change. +- Fixed cargo scooters working with a battery in an incorrect slot (unstable only). +- Fixed cargo scooters and volatile fulgurium fuel rods being craftable by anyone (unstable only). +- Fixed misaligned nav terminal and status monitor in pirate humpback. +- Fixed crash when ordering friendly NPCs (e.g. hostages) to return to the sub. + --------------------------------------------------------------------------------------------------------- v0.1500.3.0 --------------------------------------------------------------------------------------------------------- diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatch.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatch.cs index a2cc3d4e3..d6560df72 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatch.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatch.cs @@ -3,6 +3,9 @@ // file 'LICENSE.txt', which is part of this source code package. using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; using System.Text; namespace Microsoft.Xna.Framework.Graphics @@ -25,15 +28,58 @@ namespace Microsoft.Xna.Framework.Graphics /// public class SpriteBatch : GraphicsResource, ISpriteBatch { + public struct EffectWithParams + { + private static readonly Dictionary parameterSetters; + private static readonly object[] setterParams = new object[1]; + + static EffectWithParams() + { + parameterSetters = new Dictionary(); + foreach (var method in typeof(EffectParameter).GetMethods()) + { + if (method.Name.Equals("SetValue", StringComparison.InvariantCulture) + && method.GetParameters() is { Length: 1 } parameters) + { + var type = parameters[0].ParameterType; + parameterSetters[type] = method; + foreach (var derived in Assembly.GetAssembly(type).GetTypes().Where(t => t.IsSubclassOf(type))) + { + parameterSetters[derived] = method; + } + } + } + } + + public Effect Effect; + public Dictionary Params; + + public EffectWithParams(Effect effect, Dictionary parameters = null) + { + Effect = effect; + Params = parameters; + } + + internal void Apply() + { + foreach (var (paramName, paramValue) in Params) + { + setterParams[0] = paramValue; + parameterSetters[paramValue.GetType()].Invoke(Effect.Parameters[paramName], setterParams); + } + Effect.CurrentTechnique.Passes[0].Apply(); + } + } + #region Private Fields readonly SpriteBatcher _batcher; SpriteSortMode _sortMode; BlendState _blendState; SamplerState _samplerState; - DepthStencilState _depthStencilState; - RasterizerState _rasterizerState; - Effect _effect; + DepthStencilState _depthStencilState; + RasterizerState _rasterizerState; + EffectWithParams _effect; bool _beginCalled; Effect _spriteEffect; @@ -60,7 +106,7 @@ namespace Microsoft.Xna.Framework.Graphics if (graphicsDevice == null) { throw new ArgumentNullException ("graphicsDevice", FrameworkResources.ResourceCreationWhenDeviceIsNull); - } + } this.GraphicsDevice = graphicsDevice; @@ -107,7 +153,7 @@ namespace Microsoft.Xna.Framework.Graphics _samplerState = samplerState ?? SamplerState.LinearClamp; _depthStencilState = depthStencilState ?? DepthStencilState.None; _rasterizerState = rasterizerState ?? RasterizerState.CullCounterClockwise; - _effect = effect; + _effect = new EffectWithParams(effect); _matrix = transformMatrix; // Setup things now so a user can change them. @@ -119,6 +165,11 @@ namespace Microsoft.Xna.Framework.Graphics _beginCalled = true; } + /// + /// Returns the current effect. + /// + public Effect GetCurrentEffect() => _effect.Effect; + /// /// Flushes all batched text and sprites to the screen. /// @@ -132,18 +183,31 @@ namespace Microsoft.Xna.Framework.Graphics if (_sortMode != SpriteSortMode.Immediate) Setup(); - - _batcher.DrawBatch(_sortMode, _effect); + + _batcher.DrawBatch(_sortMode, _spritePass); } - - void Setup() + + /// + /// Swaps the current effect. + /// + public void SwapEffect(Effect effect = null, Dictionary parameters = null) + { + _effect = new EffectWithParams(effect, parameters); + } + + public void SwapEffect(EffectWithParams effectWithParams) + { + _effect = effectWithParams; + } + + void Setup() { var gd = GraphicsDevice; gd.BlendState = _blendState; gd.DepthStencilState = _depthStencilState; gd.RasterizerState = _rasterizerState; gd.SamplerStates[0] = _samplerState; - + var vp = gd.Viewport; if ((vp.Width != _lastViewport.Width) || (vp.Height != _lastViewport.Height)) { @@ -169,7 +233,7 @@ namespace Microsoft.Xna.Framework.Graphics _spritePass.Apply(); } - + void CheckValid(Texture2D texture) { if (texture == null) @@ -279,6 +343,7 @@ namespace Microsoft.Xna.Framework.Graphics { var item = _batcher.CreateBatchItem(); item.Texture = texture; + item.Effect = _effect; item.SortKey = sortKey; @@ -315,6 +380,7 @@ namespace Microsoft.Xna.Framework.Graphics var item = _batcher.CreateBatchItem(); item.Texture = texture; + item.Effect = _effect; // set SortKey based on SpriteSortMode. switch ( _sortMode ) @@ -332,9 +398,9 @@ namespace Microsoft.Xna.Framework.Graphics item.SortKey = -layerDepth; break; } - + origin = origin * scale; - + float w, h; if (sourceRectangle.HasValue) { @@ -353,7 +419,7 @@ namespace Microsoft.Xna.Framework.Graphics _texCoordTL = Vector2.Zero; _texCoordBR = Vector2.One; } - + if ((effects & SpriteEffects.FlipVertically) != 0) { var temp = _texCoordBR.Y; @@ -366,7 +432,7 @@ namespace Microsoft.Xna.Framework.Graphics _texCoordBR.X = _texCoordTL.X; _texCoordTL.X = temp; } - + if (rotation == 0f) { item.Set(position.X - origin.X, @@ -393,7 +459,7 @@ namespace Microsoft.Xna.Framework.Graphics _texCoordBR, layerDepth); } - + FlushIfNeeded(); } @@ -444,9 +510,10 @@ namespace Microsoft.Xna.Framework.Graphics float layerDepth) { CheckValid(texture); - + var item = _batcher.CreateBatchItem(); item.Texture = texture; + item.Effect = _effect; // set SortKey based on SpriteSortMode. switch ( _sortMode ) @@ -478,7 +545,7 @@ namespace Microsoft.Xna.Framework.Graphics else origin.X = origin.X * (float)destinationRectangle.Width * texture.TexelWidth; if(srcRect.Height != 0) - origin.Y = origin.Y * (float)destinationRectangle.Height / (float)srcRect.Height; + origin.Y = origin.Y * (float)destinationRectangle.Height / (float)srcRect.Height; else origin.Y = origin.Y * (float)destinationRectangle.Height * texture.TexelHeight; } @@ -486,11 +553,11 @@ namespace Microsoft.Xna.Framework.Graphics { _texCoordTL = Vector2.Zero; _texCoordBR = Vector2.One; - + origin.X = origin.X * (float)destinationRectangle.Width * texture.TexelWidth; origin.Y = origin.Y * (float)destinationRectangle.Height * texture.TexelHeight; } - + if ((effects & SpriteEffects.FlipVertically) != 0) { var temp = _texCoordBR.Y; @@ -539,7 +606,7 @@ namespace Microsoft.Xna.Framework.Graphics { if (_sortMode == SpriteSortMode.Immediate) { - _batcher.DrawBatch(_sortMode, _effect); + _batcher.DrawBatch(_sortMode, _spritePass); } } @@ -553,10 +620,11 @@ namespace Microsoft.Xna.Framework.Graphics public void Draw (Texture2D texture, Vector2 position, Rectangle? sourceRectangle, Color color) { CheckValid(texture); - + var item = _batcher.CreateBatchItem(); item.Texture = texture; - + item.Effect = _effect; + // set SortKey based on SpriteSortMode. item.SortKey = _sortMode == SpriteSortMode.Texture ? texture.SortingKey : 0; @@ -600,13 +668,14 @@ namespace Microsoft.Xna.Framework.Graphics public void Draw (Texture2D texture, Rectangle destinationRectangle, Rectangle? sourceRectangle, Color color) { CheckValid(texture); - + var item = _batcher.CreateBatchItem(); item.Texture = texture; - + item.Effect = _effect; + // set SortKey based on SpriteSortMode. item.SortKey = _sortMode == SpriteSortMode.Texture ? texture.SortingKey : 0; - + if (sourceRectangle.HasValue) { var srcRect = sourceRectangle.GetValueOrDefault(); @@ -629,7 +698,7 @@ namespace Microsoft.Xna.Framework.Graphics _texCoordTL, _texCoordBR, 0); - + FlushIfNeeded(); } @@ -642,13 +711,14 @@ namespace Microsoft.Xna.Framework.Graphics public void Draw (Texture2D texture, Vector2 position, Color color) { CheckValid(texture); - + var item = _batcher.CreateBatchItem(); item.Texture = texture; - + item.Effect = _effect; + // set SortKey based on SpriteSortMode. item.SortKey = _sortMode == SpriteSortMode.Texture ? texture.SortingKey : 0; - + item.Set(position.X, position.Y, texture.Width, @@ -670,13 +740,14 @@ namespace Microsoft.Xna.Framework.Graphics public void Draw(Texture2D texture, Rectangle destinationRectangle, Color color) { CheckValid(texture); - + var item = _batcher.CreateBatchItem(); item.Texture = texture; - + item.Effect = _effect; + // set SortKey based on SpriteSortMode. item.SortKey = _sortMode == SpriteSortMode.Texture ? texture.SortingKey : 0; - + item.Set(destinationRectangle.X, destinationRectangle.Y, destinationRectangle.Width, @@ -685,7 +756,7 @@ namespace Microsoft.Xna.Framework.Graphics Vector2.Zero, Vector2.One, 0); - + FlushIfNeeded(); } @@ -699,7 +770,7 @@ namespace Microsoft.Xna.Framework.Graphics public unsafe void DrawString (SpriteFont spriteFont, string text, Vector2 position, Color color) { CheckValid(spriteFont, text); - + float sortKey = (_sortMode == SpriteSortMode.Texture) ? spriteFont.Texture.SortingKey : 0; var offset = Vector2.Zero; @@ -720,7 +791,7 @@ namespace Microsoft.Xna.Framework.Graphics firstGlyphOfLine = true; continue; } - + var currentGlyphIndex = spriteFont.GetGlyphIndexOrDefault(c); var pCurrentGlyph = pGlyphs + currentGlyphIndex; @@ -737,15 +808,16 @@ namespace Microsoft.Xna.Framework.Graphics offset.X += spriteFont.Spacing + pCurrentGlyph->LeftSideBearing; } - var p = offset; + var p = offset; p.X += pCurrentGlyph->Cropping.X; p.Y += pCurrentGlyph->Cropping.Y; p += position; var item = _batcher.CreateBatchItem(); item.Texture = spriteFont.Texture; + item.Effect = _effect; item.SortKey = sortKey; - + _texCoordTL.X = pCurrentGlyph->BoundsInTexture.X * spriteFont.Texture.TexelWidth; _texCoordTL.Y = pCurrentGlyph->BoundsInTexture.Y * spriteFont.Texture.TexelHeight; _texCoordBR.X = (pCurrentGlyph->BoundsInTexture.X + pCurrentGlyph->BoundsInTexture.Width) * spriteFont.Texture.TexelWidth; @@ -759,7 +831,7 @@ namespace Microsoft.Xna.Framework.Graphics _texCoordTL, _texCoordBR, 0); - + offset.X += pCurrentGlyph->Width + pCurrentGlyph->RightSideBearing; } @@ -804,7 +876,7 @@ namespace Microsoft.Xna.Framework.Graphics float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth) { CheckValid(spriteFont, text); - + float sortKey = 0; // set SortKey based on SpriteSortMode. switch (_sortMode) @@ -831,7 +903,7 @@ namespace Microsoft.Xna.Framework.Graphics if (flippedVert || flippedHorz) { Vector2 size; - + var source = new SpriteFont.CharacterSource(text); spriteFont.MeasureString(ref source, out size); @@ -847,7 +919,7 @@ namespace Microsoft.Xna.Framework.Graphics flipAdjustment.Y = spriteFont.LineSpacing - size.Y; } } - + Matrix transformation = Matrix.Identity; float cos = 0, sin = 0; if (rotation == 0) @@ -866,7 +938,7 @@ namespace Microsoft.Xna.Framework.Graphics transformation.M21 = (flippedVert ? -scale.Y : scale.Y) * (-sin); transformation.M22 = (flippedVert ? -scale.Y : scale.Y) * cos; transformation.M41 = (((flipAdjustment.X - origin.X) * transformation.M11) + (flipAdjustment.Y - origin.Y) * transformation.M21) + position.X; - transformation.M42 = (((flipAdjustment.X - origin.X) * transformation.M12) + (flipAdjustment.Y - origin.Y) * transformation.M22) + position.Y; + transformation.M42 = (((flipAdjustment.X - origin.X) * transformation.M12) + (flipAdjustment.Y - origin.Y) * transformation.M22) + position.Y; } var offset = Vector2.Zero; @@ -916,15 +988,16 @@ namespace Microsoft.Xna.Framework.Graphics Vector2.Transform(ref p, ref transformation, out p); - var item = _batcher.CreateBatchItem(); + var item = _batcher.CreateBatchItem(); item.Texture = spriteFont.Texture; + item.Effect = _effect; item.SortKey = sortKey; - + _texCoordTL.X = pCurrentGlyph->BoundsInTexture.X * spriteFont.Texture.TexelWidth; _texCoordTL.Y = pCurrentGlyph->BoundsInTexture.Y * spriteFont.Texture.TexelHeight; _texCoordBR.X = (pCurrentGlyph->BoundsInTexture.X + pCurrentGlyph->BoundsInTexture.Width) * spriteFont.Texture.TexelWidth; _texCoordBR.Y = (pCurrentGlyph->BoundsInTexture.Y + pCurrentGlyph->BoundsInTexture.Height) * spriteFont.Texture.TexelHeight; - + if ((effects & SpriteEffects.FlipVertically) != 0) { var temp = _texCoordBR.Y; @@ -964,7 +1037,7 @@ namespace Microsoft.Xna.Framework.Graphics _texCoordBR, layerDepth); } - + offset.X += pCurrentGlyph->Width + pCurrentGlyph->RightSideBearing; } @@ -982,7 +1055,7 @@ namespace Microsoft.Xna.Framework.Graphics public unsafe void DrawString (SpriteFont spriteFont, StringBuilder text, Vector2 position, Color color) { CheckValid(spriteFont, text); - + float sortKey = (_sortMode == SpriteSortMode.Texture) ? spriteFont.Texture.SortingKey : 0; var offset = Vector2.Zero; @@ -1020,15 +1093,16 @@ namespace Microsoft.Xna.Framework.Graphics offset.X += spriteFont.Spacing + pCurrentGlyph->LeftSideBearing; } - var p = offset; + var p = offset; p.X += pCurrentGlyph->Cropping.X; p.Y += pCurrentGlyph->Cropping.Y; p += position; - + var item = _batcher.CreateBatchItem(); item.Texture = spriteFont.Texture; + item.Effect = _effect; item.SortKey = sortKey; - + _texCoordTL.X = pCurrentGlyph->BoundsInTexture.X * spriteFont.Texture.TexelWidth; _texCoordTL.Y = pCurrentGlyph->BoundsInTexture.Y * spriteFont.Texture.TexelHeight; _texCoordBR.X = (pCurrentGlyph->BoundsInTexture.X + pCurrentGlyph->BoundsInTexture.Width) * spriteFont.Texture.TexelWidth; @@ -1087,7 +1161,7 @@ namespace Microsoft.Xna.Framework.Graphics float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth) { CheckValid(spriteFont, text); - + float sortKey = 0; // set SortKey based on SpriteSortMode. switch (_sortMode) @@ -1129,7 +1203,7 @@ namespace Microsoft.Xna.Framework.Graphics flipAdjustment.Y = spriteFont.LineSpacing - size.Y; } } - + Matrix transformation = Matrix.Identity; float cos = 0, sin = 0; if (rotation == 0) @@ -1148,7 +1222,7 @@ namespace Microsoft.Xna.Framework.Graphics transformation.M21 = (flippedVert ? -scale.Y : scale.Y) * (-sin); transformation.M22 = (flippedVert ? -scale.Y : scale.Y) * cos; transformation.M41 = (((flipAdjustment.X - origin.X) * transformation.M11) + (flipAdjustment.Y - origin.Y) * transformation.M21) + position.X; - transformation.M42 = (((flipAdjustment.X - origin.X) * transformation.M12) + (flipAdjustment.Y - origin.Y) * transformation.M22) + position.Y; + transformation.M42 = (((flipAdjustment.X - origin.X) * transformation.M12) + (flipAdjustment.Y - origin.Y) * transformation.M22) + position.Y; } var offset = Vector2.Zero; @@ -1197,16 +1271,17 @@ namespace Microsoft.Xna.Framework.Graphics p.Y += pCurrentGlyph->Cropping.Y; Vector2.Transform(ref p, ref transformation, out p); - - var item = _batcher.CreateBatchItem(); + + var item = _batcher.CreateBatchItem(); item.Texture = spriteFont.Texture; + item.Effect = _effect; item.SortKey = sortKey; - + _texCoordTL.X = pCurrentGlyph->BoundsInTexture.X * (float)spriteFont.Texture.TexelWidth; _texCoordTL.Y = pCurrentGlyph->BoundsInTexture.Y * (float)spriteFont.Texture.TexelHeight; _texCoordBR.X = (pCurrentGlyph->BoundsInTexture.X + pCurrentGlyph->BoundsInTexture.Width) * (float)spriteFont.Texture.TexelWidth; _texCoordBR.Y = (pCurrentGlyph->BoundsInTexture.Y + pCurrentGlyph->BoundsInTexture.Height) * (float)spriteFont.Texture.TexelHeight; - + if ((effects & SpriteEffects.FlipVertically) != 0) { var temp = _texCoordBR.Y; diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatchItem.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatchItem.cs index d0ebb45e5..484e521d4 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatchItem.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatchItem.cs @@ -9,6 +9,7 @@ namespace Microsoft.Xna.Framework.Graphics internal class SpriteBatchItem : IComparable { public Texture2D Texture; + public SpriteBatch.EffectWithParams Effect; public float SortKey; public VertexPositionColorTexture vertexTL; @@ -20,7 +21,7 @@ namespace Microsoft.Xna.Framework.Graphics vertexTL = new VertexPositionColorTexture(); vertexTR = new VertexPositionColorTexture(); vertexBL = new VertexPositionColorTexture(); - vertexBR = new VertexPositionColorTexture(); + vertexBR = new VertexPositionColorTexture(); } public void Set ( float x, float y, float dx, float dy, float w, float h, float sin, float cos, Color color, Vector2 texCoordTL, Vector2 texCoordBR, float depth ) diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatcher.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatcher.cs index 036fd783c..948d0078c 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatcher.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatcher.cs @@ -11,7 +11,7 @@ namespace Microsoft.Xna.Framework.Graphics /// This class handles the queueing of batch items into the GPU by creating the triangle tesselations /// that are used to draw the sprite textures. This class supports int.MaxValue number of sprites to be /// batched and will process them into short.MaxValue groups (strided by 6 for the number of vertices - /// sent to the GPU). + /// sent to the GPU). /// internal class SpriteBatcher { @@ -68,7 +68,7 @@ namespace Microsoft.Xna.Framework.Graphics } /// - /// Reuse a previously allocated SpriteBatchItem from the item pool. + /// Reuse a previously allocated SpriteBatchItem from the item pool. /// if there is none available grow the pool and initialize new items. /// /// @@ -143,12 +143,8 @@ namespace Microsoft.Xna.Framework.Graphics /// overflow the 16 bit array indices for vertices. /// /// The type of depth sorting desired for the rendering. - /// The custom effect to apply to the drawn geometry - public unsafe void DrawBatch(SpriteSortMode sortMode, Effect effect) + public unsafe void DrawBatch(SpriteSortMode sortMode, EffectPass defaultSpritePass) { - if (effect != null && effect.IsDisposed) - throw new ObjectDisposedException("effect"); - // nothing to do if (_batchItemCount == 0) return; @@ -180,6 +176,7 @@ namespace Microsoft.Xna.Framework.Graphics var startIndex = 0; var index = 0; Texture2D tex = null; + SpriteBatch.EffectWithParams effect = default; int numBatchesToProcess = batchCount; if (numBatchesToProcess > MaxBatchSize) @@ -196,12 +193,24 @@ namespace Microsoft.Xna.Framework.Graphics { SpriteBatchItem item = _batchItemList[batchIndex]; // if the texture changed, we need to flush and bind the new texture - var shouldFlush = !ReferenceEquals(item.Texture, tex); + var shouldFlush = + !ReferenceEquals(item.Texture, tex) + || !ReferenceEquals(item.Effect.Effect, effect.Effect) + || !ReferenceEquals(item.Effect.Params, effect.Params); if (shouldFlush) { - FlushVertexArray(startIndex, index, effect, tex); + FlushVertexArray(startIndex, index, effect.Effect, tex); tex = item.Texture; + effect = item.Effect; + if (effect.Effect is null || effect.Params is null) + { + defaultSpritePass.Apply(); + } + else + { + effect.Apply(); + } startIndex = index = 0; vertexArrayPtr = vertexArrayFixedPtr; _device.Textures[0] = tex; @@ -215,15 +224,16 @@ namespace Microsoft.Xna.Framework.Graphics // Release the texture. item.Texture = null; + item.Effect = default; } } // flush the remaining vertexArray data - FlushVertexArray(startIndex, index, effect, tex); + FlushVertexArray(startIndex, index, effect.Effect, tex); // Update our batch count to continue the process of culling down // large batches batchCount -= numBatchesToProcess; } - // return items to the pool. + // return items to the pool. _batchItemCount = 0; }