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 000000000..e9784b881 Binary files /dev/null and b/Barotrauma/BarotraumaClient/Content/Effects/thresholdtint.xnb differ diff --git a/Barotrauma/BarotraumaClient/Content/Effects/thresholdtint_opengl.xnb b/Barotrauma/BarotraumaClient/Content/Effects/thresholdtint_opengl.xnb new file mode 100644 index 000000000..9a7b8c92d Binary files /dev/null and b/Barotrauma/BarotraumaClient/Content/Effects/thresholdtint_opengl.xnb differ 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 000000000..77afc5ae9 Binary files /dev/null and b/Barotrauma/BarotraumaShared/TintTest.png differ 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; }