diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index c27aab975..c5b38a31e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -623,6 +623,7 @@ namespace Barotrauma bool removeOnDeath = inc.ReadBoolean(); ch.ChangeSavedStatValue((StatTypes)statType, statValue, statIdentifier, removeOnDeath); } + ch.ExperiencePoints = inc.ReadUInt16(); return ch; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs index 5835cd675..f4a5f282c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs @@ -1,10 +1,4 @@ -using Microsoft.Xna.Framework; -using System.Collections.Generic; -using Barotrauma.IO; -using System.Linq; -using System.Xml.Linq; - -namespace Barotrauma +namespace Barotrauma { partial class AfflictionHusk : Affliction { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index d7b75bbd3..73e11cd6a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -30,7 +30,7 @@ namespace Barotrauma get { return alignment; } set { - if (alignment == value) return; + if (alignment == value) { return; } alignment = value; UpdateAlignment(); } @@ -48,108 +48,18 @@ namespace Barotrauma private float bloodParticleTimer; - // healing interface - private GUIFrame healthInterfaceFrame; - private GUIFrame healthWindow; private GUITextBlock deadIndicator; - private GUIComponent lowSkillIndicator; + //private GUIComponent lowSkillIndicator; - private GUILayoutGroup cprLayout; - private GUIFrame cprFrame; private GUIButton cprButton; private GUIListBox afflictionTooltip; private static readonly Color oxygenLowGrainColor = new Color(0.1f, 0.1f, 0.1f, 1f); - private struct HeartratePosition - { - public float Time; - public float Height; - - public HeartratePosition ScaleHeight(float scale) - { - return new HeartratePosition - { - Time = this.Time, - Height = this.Height * scale - }; - } - - public HeartratePosition ScaleTime(float scale) - { - return new HeartratePosition - { - Time = this.Time * scale, - Height = this.Height - }; - } - - public HeartratePosition AddTime(float time) - { - return new HeartratePosition - { - Time = this.Time + time, - Height = this.Height - }; - } - - public static IEnumerable ScaleAndDisplace(IEnumerable positions, float heightScale, float timeScale, float timeAdd) - { - HeartratePosition prevPos = new HeartratePosition - { - Time = 0.0f, - Height = 0.0f - }; - bool wrapped = false; - foreach (HeartratePosition pos in positions) - { - HeartratePosition newPos = pos.ScaleHeight(heightScale).ScaleTime(timeScale).AddTime(timeAdd); - if (newPos.Time > 1.0f) - { - if (!wrapped) - { - yield return new HeartratePosition - { - Time = 1.0f, - Height = (newPos.Height - prevPos.Height) / (newPos.Time - prevPos.Time) * (1.0f - prevPos.Time) + prevPos.Height - }; - yield return new HeartratePosition - { - Time = 0.0f, - Height = (newPos.Height - prevPos.Height) / (newPos.Time - prevPos.Time) * (1.0f - prevPos.Time) + prevPos.Height - }; - wrapped = true; - } - newPos.Time -= 1.0f; - } - prevPos = newPos; - yield return newPos; - } - } - } - private List heartratePositions; - private float currentHeartrateTime; - private float heartbeatTimer; - private static Texture2D heartrateFade; - - private readonly HeartratePosition[] heartbeatPattern = - { - new HeartratePosition() { Time = 0.0f, Height = 0.0f }, - new HeartratePosition() { Time = 0.15f, Height = 0.2f }, - new HeartratePosition() { Time = 0.2f, Height = -0.2f }, - new HeartratePosition() { Time = 0.36f, Height = 0.0f }, - new HeartratePosition() { Time = 0.43f, Height = 0.8f }, - new HeartratePosition() { Time = 0.57f, Height = -0.8f }, - new HeartratePosition() { Time = 0.64f, Height = 0.0f }, - new HeartratePosition() { Time = 0.8f, Height = 0.2f }, - new HeartratePosition() { Time = 0.85f, Height = -0.2f }, - new HeartratePosition() { Time = 1.0f, Height = 0.0f }, - }; - private SpriteSheet limbIndicatorOverlay; private float limbIndicatorOverlayAnimState; @@ -166,12 +76,9 @@ namespace Barotrauma private GUIProgressBar healthWindowHealthBarShadow; private GUITextBlock characterName; - private GUIFrame afflictionInfoFrame; private GUIListBox afflictionIconContainer; - private GUIListBox afflictionInfoContainer; private GUILayoutGroup treatmentLayout; private GUIListBox recommendedTreatmentContainer; - private GUITextBlock selectedLimbText; private float distortTimer; @@ -255,6 +162,12 @@ namespace Barotrauma get { return cprButton; } } + public GUIComponent InventorySlotContainer + { + get; + private set; + } + public float HealthBarPulsateTimer { get { return healthBarPulsateTimer; } @@ -279,56 +192,90 @@ namespace Barotrauma character.OnAttacked += OnAttacked; - bool horizontal = true; + healthWindow = new GUIFrame(new RectTransform(new Vector2(0.35f, 0.6f), GUI.Canvas, anchor: Anchor.Center, scaleBasis: ScaleBasis.Smallest), style: "GUIFrameListBox"); - healthBarHolder = new GUIFrame(new RectTransform(Point.Zero, GUI.Canvas), style: null) + var healthWindowVerticalLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.95f), healthWindow.RectTransform, Anchor.Center)) { - HoverCursor = CursorState.Hand + Stretch = true }; - healthBarHolder.RectTransform.AbsoluteOffset = HUDLayoutSettings.HealthBarArea.Location; - healthBarHolder.RectTransform.NonScaledSize = HUDLayoutSettings.HealthBarArea.Size; - healthBarHolder.RectTransform.RelativeOffset = Vector2.Zero; - - healthBarShadow = new GUIProgressBar(new RectTransform(Vector2.One, healthBarHolder.RectTransform, Anchor.BottomRight), - barSize: 1.0f, color: Color.Green, style: horizontal ? "CharacterHealthBar" : "GUIProgressBarVertical", showFrame: false) + var nameContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.2f), healthWindowVerticalLayout.RectTransform) { MinSize = new Point(0, 20) }, isHorizontal: true) { - IsHorizontal = horizontal - }; - healthBarShadow.Visible = false; - healthShadowSize = 1.0f; - - healthBar = new GUIProgressBar(new RectTransform(Vector2.One, healthBarHolder.RectTransform, Anchor.BottomRight), - barSize: 1.0f, color: GUI.Style.HealthBarColorHigh, style: horizontal ? "CharacterHealthBar" : "GUIProgressBarVertical") - { - HoverCursor = CursorState.Hand, - ToolTip = TextManager.GetWithVariable("hudbutton.healthinterface", "[key]", GameMain.Config.KeyBindText(InputType.Health)), - Enabled = true, - IsHorizontal = horizontal + Stretch = true }; - healthInterfaceFrame = new GUIFrame(new RectTransform(new Vector2(0.7f, 0.55f), GUI.Canvas, anchor: Anchor.Center, scaleBasis: ScaleBasis.Smallest), style: "ItemUI"); + new GUICustomComponent(new RectTransform(new Vector2(0.2f, 1.0f), nameContainer.RectTransform, Anchor.CenterLeft), + onDraw: (spriteBatch, component) => + { + character.Info?.DrawPortrait(spriteBatch, new Vector2(component.Rect.X, component.Rect.Center.Y - component.Rect.Width / 2), Vector2.Zero, component.Rect.Width, false, openHealthWindow?.Character != Character.Controlled); + }); + characterName = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), nameContainer.RectTransform), "", textAlignment: Alignment.CenterLeft, font: GUI.SubHeadingFont) + { + AutoScaleHorizontal = true + }; + new GUICustomComponent(new RectTransform(new Vector2(0.2f, 1.0f), nameContainer.RectTransform), + onDraw: (spriteBatch, component) => + { + character.Info?.DrawJobIcon(spriteBatch, component.Rect, openHealthWindow?.Character != Character.Controlled); + }); - var healthInterfaceLayout = new GUILayoutGroup(new RectTransform(Vector2.One / 1.05f, healthInterfaceFrame.RectTransform, anchor: Anchor.Center), true); - var healthWindowContainer = new GUIFrame(new RectTransform(new Vector2(0.45f, 1.0f), healthInterfaceLayout.RectTransform), style: null); + var healthBarContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.07f), healthWindowVerticalLayout.RectTransform), style: null); + var healthBarIcon = new GUIFrame(new RectTransform(new Vector2(0.095f, 1.0f), healthBarContainer.RectTransform), style: "GUIHealthBarIcon"); + healthWindowHealthBarShadow = new GUIProgressBar(new RectTransform(new Vector2(0.91f, 1.0f), healthBarContainer.RectTransform, Anchor.CenterRight), + barSize: 1.0f, color: GUI.Style.Green, style: "GUIHealthBar") + { + IsHorizontal = true + }; + healthWindowHealthBar = new GUIProgressBar(new RectTransform(new Vector2(0.91f, 1.0f), healthBarContainer.RectTransform, Anchor.CenterRight), + barSize: 1.0f, color: GUI.Style.Green, style: "GUIHealthBar") + { + IsHorizontal = true + }; - //limb selection frame - healthWindow = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.9f), healthWindowContainer.RectTransform, Anchor.CenterRight, Pivot.CenterRight), style: "GUIFrameListBox"); + //spacing + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), healthWindowVerticalLayout.RectTransform), style: null); - var healthWindowVerticalLayout = new GUILayoutGroup(new RectTransform(Vector2.One * 0.95f, healthWindow.RectTransform, Anchor.Center)) + var characterIndicatorArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.95f), healthWindowVerticalLayout.RectTransform), isHorizontal: true) { Stretch = true, - RelativeSpacing = 0.03f + //RelativeSpacing = 0.05f }; - var paddedHealthWindow = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.95f), healthWindowVerticalLayout.RectTransform), isHorizontal: true) - { - Stretch = true, - RelativeSpacing = 0.03f - }; + InventorySlotContainer = new GUICustomComponent(new RectTransform(new Vector2(0.1f, 1.0f), characterIndicatorArea.RectTransform, Anchor.TopLeft, Pivot.TopRight), + (spriteBatch, component) => + { + for (int i = 0; i < character.Inventory.Capacity; i++) + { + if (character.Inventory.SlotTypes[i] != InvSlotType.HealthInterface || Character.Controlled != Character) { continue; } - var limbSelection = new GUICustomComponent(new RectTransform(new Vector2(0.6f, 1.0f), paddedHealthWindow.RectTransform), + //don't draw the item if it's being dragged out of the slot + bool drawItem = !Inventory.DraggingItems.Any() || !Character.Inventory.GetItemsAt(i).All(it => Inventory.DraggingItems.Contains(it)) || character.Inventory.visualSlots[i].MouseOn(); + + Inventory.DrawSlot(spriteBatch, Character.Inventory, Character.Inventory.visualSlots[i], Character.Inventory.GetItemAt(i), i, drawItem, Character.Inventory.SlotTypes[i]); + + if (medUIExtra != null) + { + float overlayScale = Math.Min( + Character.Inventory.visualSlots[i].Rect.Width / (float)medUIExtra.FrameSize.X, + Character.Inventory.visualSlots[i].Rect.Height / (float)medUIExtra.FrameSize.Y); + + int frame = (int)medUIExtraAnimState; + + medUIExtra.Draw(spriteBatch, frame, Character.Inventory.visualSlots[i].Rect.Center.ToVector2(), Color.Gray, origin: medUIExtra.FrameSize.ToVector2() / 2, rotate: 0.0f, + scale: Vector2.One * overlayScale); + } + } + }, + (dt, component) => + { + if (!GameMain.Instance.Paused) + { + medUIExtraAnimState = (medUIExtraAnimState + dt * 10.0f) % 16.0f; + } + }); + + var limbSelection = new GUICustomComponent(new RectTransform(new Vector2(0.4f, 1.0f), characterIndicatorArea.RectTransform), (spriteBatch, component) => { DrawHealthWindow(spriteBatch, component.RectTransform.Rect, true); @@ -353,149 +300,31 @@ namespace Barotrauma deadIndicator.AutoScaleHorizontal = true; } - var rightSide = new GUIFrame(new RectTransform(new Vector2(0.4f, 1.0f), paddedHealthWindow.RectTransform), style: null); + afflictionIconContainer = new GUIListBox(new RectTransform(new Vector2(0.25f, 0.7f), characterIndicatorArea.RectTransform), style: null); - new GUICustomComponent(new RectTransform(new Vector2(1.0f, 0.3f), rightSide.RectTransform, Anchor.BottomRight, Pivot.BottomRight), - (sb, component) => - { - if (medUIExtra == null) { return; } - float overlayScale = Math.Min( - component.Rect.Width / (float)medUIExtra.FrameSize.X, - component.Rect.Height / (float)medUIExtra.FrameSize.Y); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), healthWindowVerticalLayout.RectTransform), + TextManager.Get("SuitableTreatments"), font: GUI.SubHeadingFont, textAlignment: Alignment.BottomCenter); - int frame = (int)medUIExtraAnimState; - - medUIExtra.Draw(sb, frame, component.Rect.Center.ToVector2(), Color.Gray, origin: medUIExtra.FrameSize.ToVector2() / 2, rotate: 0.0f, - scale: Vector2.One * overlayScale); - }, - (dt, component) => - { - if (!GameMain.Instance.Paused) - { - medUIExtraAnimState = (medUIExtraAnimState + dt * 10.0f) % 16.0f; - } - }); - - GUILayoutGroup selectedLimbLayout = new GUILayoutGroup(new RectTransform(Vector2.One, rightSide.RectTransform)); - - selectedLimbText = new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.08f), selectedLimbLayout.RectTransform), "", font: GUI.SubHeadingFont, textAlignment: Alignment.Center) - { - AutoScaleHorizontal = true - }; - - afflictionIconContainer = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.92f), selectedLimbLayout.RectTransform), style: null) - { - KeepSpaceForScrollBar = true - }; - - var healthBarContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.07f), healthWindowVerticalLayout.RectTransform), style: null); - - var healthBarIcon = new GUIFrame(new RectTransform(new Vector2(0.095f, 1.0f), healthBarContainer.RectTransform), style: "GUIHealthBarIcon"); - - healthWindowHealthBarShadow = new GUIProgressBar(new RectTransform(new Vector2(0.91f, 1.0f), healthBarContainer.RectTransform, Anchor.CenterRight), - barSize: 1.0f, color: GUI.Style.Green, style: "GUIHealthBar") - { - IsHorizontal = true - }; - healthWindowHealthBar = new GUIProgressBar(new RectTransform(new Vector2(0.91f, 1.0f), healthBarContainer.RectTransform, Anchor.CenterRight), - barSize: 1.0f, color: GUI.Style.Green, style: "GUIHealthBar") - { - IsHorizontal = true - }; - - //affliction info frame - afflictionInfoFrame = new GUIFrame(new RectTransform(new Vector2(0.55f, 1.0f), healthInterfaceLayout.RectTransform), style: null); - var paddedInfoFrame = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.9f), afflictionInfoFrame.RectTransform, Anchor.Center), style: null); - - var infoLayout = new GUILayoutGroup(new RectTransform(Vector2.One, paddedInfoFrame.RectTransform)) - { - Stretch = true, - RelativeSpacing = 0.03f - }; - - var textContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.6f), infoLayout.RectTransform), style: "GUIFrameListBox"); - - var textLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.98f), textContainer.RectTransform, Anchor.Center, Pivot.Center)) - { - Stretch = true, - RelativeSpacing = 0.03f, - CanBeFocused = true - }; - - textLayout.RectTransform.RelativeOffset = new Vector2(0, 0.025f); - - var nameContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.2f), textLayout.RectTransform) { MinSize = new Point(0, 20) }, isHorizontal: true) - { - Stretch = true - }; - - new GUICustomComponent(new RectTransform(new Vector2(0.2f, 1.0f), nameContainer.RectTransform, Anchor.CenterLeft), - onDraw: (spriteBatch, component) => - { - character.Info?.DrawPortrait(spriteBatch, new Vector2(component.Rect.X, component.Rect.Center.Y - component.Rect.Width / 2), Vector2.Zero, component.Rect.Width, false, openHealthWindow?.Character != Character.Controlled); - }); - characterName = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), nameContainer.RectTransform), "", textAlignment: Alignment.CenterLeft, font: GUI.SubHeadingFont) - { - AutoScaleHorizontal = true - }; - new GUICustomComponent(new RectTransform(new Vector2(0.2f, 1.0f), nameContainer.RectTransform), - onDraw: (spriteBatch, component) => - { - character.Info?.DrawJobIcon(spriteBatch, component.Rect, openHealthWindow?.Character != Character.Controlled); - }); - - - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), textLayout.RectTransform), style: "HorizontalLine"); - - afflictionInfoContainer = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.4f), textLayout.RectTransform, Anchor.TopLeft), style: null); - - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), textLayout.RectTransform), style: "HorizontalLine"); - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), textLayout.RectTransform, Anchor.TopLeft), TextManager.Get("SuitableTreatments"), font: GUI.SubHeadingFont); - - treatmentLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), textLayout.RectTransform), true) + treatmentLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), healthWindowVerticalLayout.RectTransform), true) { Stretch = false }; - recommendedTreatmentContainer = new GUIListBox(new RectTransform(new Vector2(0.9f, 1.0f), treatmentLayout.RectTransform, Anchor.Center, Pivot.Center), isHorizontal: true, style: null) + recommendedTreatmentContainer = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), treatmentLayout.RectTransform, Anchor.Center, Pivot.Center), isHorizontal: true, style: null) { - KeepSpaceForScrollBar = false + Spacing = GUI.IntScale(4), + KeepSpaceForScrollBar = false, + ScrollBarVisible = false, + AutoHideScrollBar = false + }; + new GUITextBlock(new RectTransform(Vector2.One, recommendedTreatmentContainer.Content.RectTransform), TextManager.Get("none"), textAlignment: Alignment.Center) + { + CanBeFocused = false }; - lowSkillIndicator = new GUIImage(new RectTransform(new Vector2(0.1f, 1.0f), treatmentLayout.RectTransform, Anchor.TopLeft, Pivot.Center), - style: "GUINotificationButton") - { - ToolTip = TextManager.Get("lowmedicalskillwarning"), - Color = GUI.Style.Orange, - HoverColor = Color.Lerp(GUI.Style.Orange, Color.White, 0.5f), - PressedColor = Color.Lerp(GUI.Style.Orange, Color.White, 0.5f), - Visible = false - }; - lowSkillIndicator.RectTransform.MaxSize = new Point(lowSkillIndicator.Rect.Height); + characterIndicatorArea.Recalculate(); - var tempFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), textLayout.RectTransform), style: null); - - cprLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), infoLayout.RectTransform), true) - { - Stretch = true - }; - - cprFrame = new GUIFrame(new RectTransform(new Vector2(0.7f, 1.0f), cprLayout.RectTransform), style: "GUIFrameListBox"); - - heartrateFade ??= TextureLoader.FromFile("Content/UI/Health/HeartrateFade.png"); - - new GUICustomComponent(new RectTransform(Vector2.One * 0.95f, cprFrame.RectTransform, Anchor.Center), DrawHeartrate, UpdateHeartrate); - - heartbeatTimer = 0.46f; - - heartratePositions = new List - { - heartbeatPattern.First(), - heartbeatPattern.Last() - }; - - cprButton = new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), cprLayout.RectTransform, scaleBasis: ScaleBasis.Smallest), text: "", style: "CPRButton") + cprButton = new GUIButton(new RectTransform(new Vector2(afflictionIconContainer.RectTransform.RelativeSize.X, 0.3f), characterIndicatorArea.RectTransform, Anchor.BottomRight, scaleBasis: ScaleBasis.Smallest), text: "", style: "CPRButton") { OnClicked = (button, userData) => { @@ -518,9 +347,34 @@ namespace Barotrauma return true; }, ToolTip = TextManager.Get("doctor.cprobjective"), + IgnoreLayoutGroups = true, Visible = false }; + healthBarHolder = new GUIFrame(new RectTransform(Point.Zero, GUI.Canvas), style: null) + { + HoverCursor = CursorState.Hand + }; + + healthBarHolder.RectTransform.AbsoluteOffset = HUDLayoutSettings.HealthBarArea.Location; + healthBarHolder.RectTransform.NonScaledSize = HUDLayoutSettings.HealthBarArea.Size; + healthBarHolder.RectTransform.RelativeOffset = Vector2.Zero; + + healthBarShadow = new GUIProgressBar(new RectTransform(Vector2.One, healthBarHolder.RectTransform, Anchor.BottomRight), + barSize: 1.0f, color: Color.Green, style: "CharacterHealthBar", showFrame: false) + { + Visible = false + }; + healthShadowSize = 1.0f; + + healthBar = new GUIProgressBar(new RectTransform(Vector2.One, healthBarHolder.RectTransform, Anchor.BottomRight), + barSize: 1.0f, color: GUI.Style.HealthBarColorHigh, style: "CharacterHealthBar") + { + HoverCursor = CursorState.Hand, + ToolTip = TextManager.GetWithVariable("hudbutton.healthinterface", "[key]", GameMain.Config.KeyBindText(InputType.Health)), + Enabled = true + }; + UpdateAlignment(); SuicideButton = new GUIButton(new RectTransform(new Vector2(0.1f, 0.02f), GUI.Canvas, Anchor.TopCenter) @@ -542,8 +396,8 @@ namespace Barotrauma } else { - var causeOfDeath = GetCauseOfDeath(); - Character.Controlled.Kill(causeOfDeath.type, causeOfDeath.affliction); + var (type, affliction) = GetCauseOfDeath(); + Character.Controlled.Kill(type, affliction); Character.Controlled = null; } } @@ -598,15 +452,15 @@ namespace Barotrauma switch (alignment) { case Alignment.Left: - healthInterfaceFrame.RectTransform.SetPosition(Anchor.BottomLeft); + healthWindow.RectTransform.SetPosition(Anchor.BottomLeft); break; case Alignment.Right: - healthInterfaceFrame.RectTransform.SetPosition(Anchor.BottomRight); + healthWindow.RectTransform.SetPosition(Anchor.BottomRight); break; } - healthInterfaceFrame.RectTransform.AbsoluteOffset = new Point(HUDLayoutSettings.Padding, screenResolution.Y - HUDLayoutSettings.ChatBoxArea.Y + HUDLayoutSettings.Padding); - healthInterfaceFrame.RectTransform.RecalculateChildren(false); + healthWindow.RectTransform.AbsoluteOffset = new Point(HUDLayoutSettings.Padding, screenResolution.Y - HUDLayoutSettings.ChatBoxArea.Y + HUDLayoutSettings.Padding); + healthWindow.RectTransform.RecalculateChildren(false); } public void UpdateClientSpecific(float deltaTime) @@ -799,7 +653,8 @@ namespace Barotrauma if (afflictionGrainStrength > 0.0f) { grainStrength = Math.Max(grainStrength, affliction.GetScreenGrainStrength()); - grainColor = Color.Lerp(grainColor, Color.White, (float)Math.Pow(1.0f - oxygenLowStrength, 2)); + Color afflictionGrainColor = affliction.GetActiveEffect()?.GrainColor ?? Color.White; + grainColor = Color.Lerp(grainColor, afflictionGrainColor, (float)Math.Pow(1.0f - oxygenLowStrength, 2)); } } foreach (LimbHealth limbHealth in limbHealths) @@ -846,7 +701,7 @@ namespace Barotrauma } else if (openHealthWindow == this) { - if (HUD.CloseHUD(healthInterfaceFrame.Rect)) + if (HUD.CloseHUD(healthWindow.Rect)) { //emulate a Health input to get the character to deselect the item server-side if (GameMain.Client != null) @@ -856,6 +711,20 @@ namespace Barotrauma OpenHealthWindow = null; } + foreach (GUIComponent afflictionIcon in afflictionIconContainer.Content.Children) + { + if (!(afflictionIcon.UserData is Affliction affliction)) { continue; } + var btn = afflictionIcon.GetChild(); + if (affliction.AppliedAsFailedTreatmentTime > Timing.TotalTime - 1.0 && btn.FlashTimer <= 0.0f) + { + btn.Flash(GUI.Style.Red); + } + else if (affliction.AppliedAsSuccessfulTreatmentTime > Timing.TotalTime - 1.0 && btn.FlashTimer <= 0.0f) + { + btn.Flash(GUI.Style.Green); + } + } + if (GUI.MouseOn != null && GUI.MouseOn.UserData is string str && str == "selectaffliction") { Affliction affliction = GUI.MouseOn.Parent.UserData as Affliction; @@ -872,7 +741,17 @@ namespace Barotrauma int height = afflictionTooltip.Content.Children.Sum(c => c.Rect.Height) + 10; afflictionTooltip.RectTransform.Resize(new Point(afflictionTooltip.Rect.Width, height), true); - afflictionTooltip.RectTransform.AbsoluteOffset = new Point(GUI.MouseOn.Rect.Right, GUI.MouseOn.Rect.Y); + if (Alignment == Alignment.Right) + { + afflictionTooltip.RectTransform.AbsoluteOffset = new Point(GUI.MouseOn.Rect.X, GUI.MouseOn.Rect.Y); + afflictionTooltip.RectTransform.Pivot = Pivot.TopRight; + } + else + { + afflictionTooltip.RectTransform.AbsoluteOffset = new Point(GUI.MouseOn.Rect.Right, GUI.MouseOn.Rect.Y); + afflictionTooltip.RectTransform.Anchor = Anchor.TopLeft; + } + afflictionTooltip.ScrollBarVisible = false; var labelContainer = afflictionTooltip.Content.GetChildByUserData("label"); @@ -915,6 +794,13 @@ namespace Barotrauma UpdateAfflictionContainer(selectedLimb); currentDisplayedLimb = selectedLimb; } + + foreach (GUIComponent component in recommendedTreatmentContainer.Content.Children) + { + var treatmentButton = component.GetChild(); + if (!(treatmentButton?.UserData is ItemPrefab itemPrefab)) { continue; } + treatmentButton.Enabled = Character.Controlled.Inventory.AllItems.Any(it => it.prefab == itemPrefab); + } } if (Character.IsDead) @@ -953,16 +839,6 @@ namespace Barotrauma openHealthWindow = null; } - lowSkillIndicator.Visible = Character.Controlled != null && Character.Controlled.GetSkillLevel("medical") < 50.0f; - lowSkillIndicator.IgnoreLayoutGroups = !lowSkillIndicator.Visible; - - recommendedTreatmentContainer.RectTransform.Resize(new Vector2(0.9f, 1.0f)); - lowSkillIndicator.RectTransform.Resize(new Vector2(0.1f, 1.0f)); - - treatmentLayout.Recalculate(); - - lowSkillIndicator.Color = new Color(lowSkillIndicator.Color, MathHelper.Lerp(0.5f, 1.0f, (float)(Math.Sin(Timing.TotalTime * 5.0f) + 1.0f) / 2.0f)); - if (Inventory.DraggingItems.Any()) { if (highlightedLimbIndex > -1) @@ -979,11 +855,6 @@ namespace Barotrauma draggingMed = null; } } - - /*if (GUI.MouseOn?.UserData is Affliction affliction) - { - ShowAfflictionInfo(affliction, afflictionInfoContainer); - }*/ } else { @@ -1038,17 +909,11 @@ namespace Barotrauma && !Character.IsDead && Character.IsKnockedDown && openHealthWindow == this; - cprButton.IgnoreLayoutGroups = !cprButton.Visible; cprButton.Selected = Character.Controlled != null && Character == Character.Controlled.SelectedCharacter && Character.Controlled.AnimController.Anim == AnimController.Animation.CPR; - cprFrame.RectTransform.Resize(new Vector2(0.7f, 1.0f)); - cprButton.RectTransform.Resize(new Vector2(1.0f, 1.0f)); - - cprLayout.Recalculate(); - deadIndicator.Visible = Character.IsDead; } @@ -1057,7 +922,7 @@ namespace Barotrauma if (GUI.DisableHUD) { return; } if (OpenHealthWindow == this) { - healthInterfaceFrame.AddToGUIUpdateList(); + healthWindow.AddToGUIUpdateList(); afflictionTooltip?.AddToGUIUpdateList(); } else if (Character.Controlled == Character && !CharacterHUD.IsCampaignInterfaceOpen) @@ -1114,22 +979,22 @@ namespace Barotrauma } - private Pair highlightedAfflictionIcon = null; + private (Affliction affliction, string text)? highlightedAfflictionIcon; public void DrawStatusHUD(SpriteBatch spriteBatch) { highlightedAfflictionIcon = null; //Rectangle interactArea = healthBar.Rect; if (Character.Controlled?.SelectedCharacter == null && openHealthWindow == null) { - List> statusIcons = new List>(); + List<(Affliction affliction, string text)> statusIcons = new List<(Affliction affliction, string text)>(); if (Character.CurrentHull == null || Character.CurrentHull.LethalPressure > 5.0f) - statusIcons.Add(new Pair(pressureAffliction, TextManager.Get("PressureHUDWarning"))); + statusIcons.Add((pressureAffliction, TextManager.Get("PressureHUDWarning"))); if (Character.CurrentHull != null && Character.OxygenAvailable < LowOxygenThreshold && oxygenLowAffliction.Strength < oxygenLowAffliction.Prefab.ShowIconThreshold) - statusIcons.Add(new Pair(oxygenLowAffliction, TextManager.Get("OxygenHUDWarning"))); + statusIcons.Add((oxygenLowAffliction, TextManager.Get("OxygenHUDWarning"))); foreach (Affliction affliction in currentDisplayedAfflictions) { - statusIcons.Add(new Pair(affliction, affliction.Prefab.Name)); + statusIcons.Add((affliction, affliction.Prefab.Name)); } Vector2 highlightedIconPos = Vector2.Zero; @@ -1146,9 +1011,9 @@ namespace Barotrauma Point pos = new Point(afflictionArea.Right - iconSize, afflictionArea.Top); - foreach (Pair statusIcon in statusIcons) + foreach (var statusIcon in statusIcons) { - Affliction affliction = statusIcon.First; + Affliction affliction = statusIcon.affliction; AfflictionPrefab afflictionPrefab = affliction.Prefab; Rectangle afflictionIconRect = new Rectangle(pos, new Point(iconSize)); @@ -1168,12 +1033,6 @@ namespace Barotrauma GUI.Style.Red * (float)((Math.Sin(affliction.DamagePerSecondTimer * MathHelper.TwoPi - MathHelper.PiOver2) + 1.0f) * 0.5f)); } - /*var slot = GUI.Style.GetComponentStyle("AfflictionIconSlot"); - slot.Sprites[highlightedIcon == statusIcon ? GUIComponent.ComponentState.Hover : GUIComponent.ComponentState.None][0].Draw( - spriteBatch, afflictionIconRect, - highlightedIcon == statusIcon ? slot.HoverColor : slot.Color);*/ - - float alphaMultiplier = highlightedAfflictionIcon == statusIcon ? 1f : 0.8f; afflictionPrefab.Icon?.Draw(spriteBatch, @@ -1191,7 +1050,7 @@ namespace Barotrauma if (highlightedAfflictionIcon != null) { - string nameTooltip = highlightedAfflictionIcon.Second; + string nameTooltip = highlightedAfflictionIcon.Value.text; Vector2 offset = GUI.Font.MeasureString(nameTooltip); GUI.DrawString(spriteBatch, @@ -1258,8 +1117,6 @@ namespace Barotrauma private void UpdateAfflictionContainer(LimbHealth selectedLimb) { - selectedLimbText.Text = selectedLimb == null ? "" : selectedLimb.Name; - if (selectedLimb == null) { afflictionIconContainer.Content.ClearChildren(); @@ -1279,28 +1136,30 @@ namespace Barotrauma private void CreateAfflictionInfos(IEnumerable afflictions) { afflictionIconContainer.ClearChildren(); - afflictionInfoContainer.ClearChildren(); - afflictionInfoContainer.UserData = null; recommendedTreatmentContainer.Content.ClearChildren(); float characterSkillLevel = Character.Controlled == null ? 0.0f : Character.Controlled.GetSkillLevel("medical"); - //random variance is 200% when the skill is 0 - //no random variance if the skill is 50 or more - float randomVariance = MathHelper.Lerp(2.0f, 0.0f, characterSkillLevel / 50.0f); - //key = item identifier //float = suitability Dictionary treatmentSuitability = new Dictionary(); - GetSuitableTreatments(treatmentSuitability, normalize: true, randomization: randomVariance); + GetSuitableTreatments(treatmentSuitability, normalize: 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; + } + } - //Affliction mostSevereAffliction = afflictions.FirstOrDefault(a => !a.Prefab.IsBuff && !afflictions.Any(a2 => !a2.Prefab.IsBuff && a2.Strength > a.Strength)) ?? afflictions.FirstOrDefault(); Affliction mostSevereAffliction = SortAfflictionsBySeverity(afflictions).FirstOrDefault(); GUIButton buttonToSelect = null; foreach (Affliction affliction in afflictions) { - var child = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), afflictionIconContainer.Content.RectTransform, Anchor.TopCenter)) + var child = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.3f), afflictionIconContainer.Content.RectTransform, Anchor.TopCenter)) { Stretch = true, UserData = affliction @@ -1345,8 +1204,9 @@ namespace Barotrauma var nameText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), child.RectTransform), affliction.Prefab.Name, font: GUI.SmallFont, textAlignment: Alignment.Center, style: "GUIToolTip"); nameText.Text = ToolBox.LimitString(nameText.Text, nameText.Font, nameText.Rect.Width); + nameText.RectTransform.MinSize = new Point(0, (int)(nameText.TextSize.Y * 1.25f)); - new GUIProgressBar(new RectTransform(new Vector2(1.0f, 0.1f), child.RectTransform), 0.0f, afflictionEffectColor, style: "GUIAfflictionBar") + new GUIProgressBar(new RectTransform(new Vector2(1.0f, 0.15f), child.RectTransform), 0.0f, afflictionEffectColor, style: "GUIAfflictionBar") { UserData = "afflictionstrength" }; @@ -1354,6 +1214,21 @@ namespace Barotrauma child.Recalculate(); } + if (!treatmentSuitability.Any()) + { + new GUITextBlock(new RectTransform(Vector2.One, recommendedTreatmentContainer.Content.RectTransform), TextManager.Get("none"), textAlignment: Alignment.Center) + { + CanBeFocused = false + }; + recommendedTreatmentContainer.ScrollBarVisible = false; + recommendedTreatmentContainer.AutoHideScrollBar = false; + } + else + { + recommendedTreatmentContainer.ScrollBarVisible = true; + recommendedTreatmentContainer.AutoHideScrollBar = true; + } + buttonToSelect?.OnClicked(buttonToSelect, "selectaffliction"); afflictionIconContainer.RecalculateChildren(); @@ -1367,15 +1242,25 @@ namespace Barotrauma if (count > 5) { break; } if (!(MapEntityPrefab.Find(name: null, identifier: treatment.Key, showErrorMessages: false) is ItemPrefab item)) { continue; } - var itemSlot = new GUIFrame(new RectTransform(new Vector2(1.0f / 7.0f, 1.0f), recommendedTreatmentContainer.Content.RectTransform, Anchor.TopLeft), + var itemSlot = new GUIFrame(new RectTransform(new Vector2(1.0f / 6.0f, 1.0f), recommendedTreatmentContainer.Content.RectTransform, Anchor.TopLeft), style: null) { UserData = item }; - var innerFrame = new GUIFrame(new RectTransform(Vector2.One, itemSlot.RectTransform, Anchor.Center, Pivot.Center, scaleBasis: ScaleBasis.Smallest), style: "GUIFrameListBox") + var innerFrame = new GUIButton(new RectTransform(Vector2.One, itemSlot.RectTransform, Anchor.Center, Pivot.Center, scaleBasis: ScaleBasis.Smallest), style: "GUIButtonRound") { - CanBeFocused = false + UserData = item, + ToolTip = $"‖color:255,255,255,255‖{item.Name}‖color:end‖" + '\n' + item.Description, + OnClicked = (btn, userdata) => + { + if (!(userdata is ItemPrefab itemPrefab)) { return false; } + var item = Character.Controlled.Inventory.AllItems.FirstOrDefault(it => it.prefab == itemPrefab); + if (item == null) { return false; } + Limb targetLimb = Character.AnimController.Limbs.FirstOrDefault(l => l.HealthIndex == selectedLimbIndex); + item.ApplyTreatment(Character.Controlled, Character, targetLimb); + return true; + } }; Sprite itemSprite = item.InventoryIcon ?? item.sprite; Color itemColor = itemSprite == item.sprite ? item.SpriteColor : item.InventoryIconColor; @@ -1383,11 +1268,11 @@ namespace Barotrauma itemSprite, scaleToFit: true) { CanBeFocused = false, - Color = itemColor, + Color = itemColor * 0.9f, HoverColor = itemColor, - SelectedColor = itemColor + SelectedColor = itemColor, + DisabledColor = itemColor * 0.7f }; - itemSlot.ToolTip = item.Name; } recommendedTreatmentContainer.RecalculateChildren(); @@ -1399,11 +1284,6 @@ namespace Barotrauma int dmgPerSecond = Math.Sign(second.DamagePerSecond - first.DamagePerSecond); return dmgPerSecond != 0 ? dmgPerSecond : Math.Sign(second.Strength - first.Strength); }); - - //afflictionIconContainer.Content.RectTransform.SortChildren((r1, r2) => - //{ - // return Math.Sign(((Affliction)r2.GUIComponent.UserData).GetVitalityDecrease(this) - ((Affliction)r1.GUIComponent.UserData).GetVitalityDecrease(this)); - //}); } private void CreateAfflictionInfoElements(GUIComponent parent, Affliction affliction) @@ -1460,7 +1340,6 @@ namespace Barotrauma affliction.Strength / affliction.Prefab.MaxStrength); description.RectTransform.Resize(new Point(description.Rect.Width, (int)(description.TextSize.Y + 10))); - //labelContainer.Recalculate(); int vitalityDecrease = (int)affliction.GetVitalityDecrease(this); if (vitalityDecrease == 0) @@ -1480,21 +1359,7 @@ namespace Barotrauma private bool SelectAffliction(GUIButton button, object userData) { - Affliction affliction = button.Parent.UserData as Affliction; - bool selected = button.Selected; - - afflictionInfoContainer.UserData = null; - afflictionInfoContainer.ClearChildren(); - if (!selected) - { - afflictionInfoContainer.UserData = affliction; - - CreateAfflictionInfoElements(afflictionInfoContainer.Content, affliction); - - afflictionInfoContainer.RecalculateChildren(); - } - foreach (var child in afflictionIconContainer.Content.Children) { GUIButton btn = child.GetChild(); @@ -1507,126 +1372,6 @@ namespace Barotrauma return false; } - private void UpdateHeartrate(float deltaTime, GUICustomComponent component) - { - if (GameMain.Instance.Paused) { return; } - - heartbeatTimer -= deltaTime * 0.5f; - - if (heartbeatTimer <= 0.0f) - { - while (heartbeatTimer <= 0.0f) { heartbeatTimer += 0.5f; } - - IEnumerable newPositions; - if (Character == null || Character.IsDead || Character.IsUnconscious) - { - newPositions = Enumerable.Repeat(new HeartratePosition { Time = currentHeartrateTime, Height = 0.0f }, 1); - } - else - { - newPositions = HeartratePosition.ScaleAndDisplace(heartbeatPattern, 1.0f, 0.1f, currentHeartrateTime); - } - - float visibleRangeStart = currentHeartrateTime - 0.35f; - if (visibleRangeStart < 0.0f) - { - visibleRangeStart += 1.0f; - } - heartratePositions.RemoveAll(hp => (hp.Time < visibleRangeStart || hp.Time > currentHeartrateTime) && - ((hp.Time < visibleRangeStart && hp.Time > currentHeartrateTime) || visibleRangeStart < currentHeartrateTime)); - - heartratePositions.AddRange(newPositions); - - if (!heartratePositions.Any(hp => hp.Time >= 1.0f)) - { - heartratePositions.Add(new HeartratePosition { Time = 1.0f, Height = 0.0f }); - } - if (!heartratePositions.Any(hp => hp.Time <= 0.0f)) - { - heartratePositions.Add(new HeartratePosition { Time = 0.0f, Height = 0.0f }); - } - } - - currentHeartrateTime += deltaTime * 0.5f; - while (currentHeartrateTime >= 1.0f) - { - currentHeartrateTime -= 1.0f; - } - } - - private void DrawHeartrate(SpriteBatch spriteBatch, GUICustomComponent component) - { - Rectangle targetRect = component.Parent.Rect; - targetRect.Location += new Point(6, 6); - targetRect.Size -= new Point(12, 12); - - //GUI.DrawRectangle(spriteBatch, targetRect, Color.Black, true); - - bool first = true; - Vector2 prevPos = Vector2.Zero; - foreach (var heartratePosition in heartratePositions.OrderBy(hp => hp.Time)) - { - Vector2 pos = new Vector2(heartratePosition.Time, -heartratePosition.Height * 0.5f + 0.5f) * targetRect.Size.ToVector2() + targetRect.Location.ToVector2(); - - if (pos.X < targetRect.Left + 1) { pos.X = targetRect.Left + 1; } - if (pos.X > targetRect.Right - 1) { pos.X = targetRect.Right - 1; } - - if (first) - { - first = false; - } - else - { - int thickness = (int)(GUI.Scale * 2.5f); - if (thickness < 1) { thickness = 1; } - GUI.DrawLine(spriteBatch, prevPos, pos, Color.Lime, 0, thickness); - GUI.DrawLine(spriteBatch, prevPos + new Vector2(0.0f, 1.0f), pos + new Vector2(0.0f, 1.0f), Color.Lime * 0.5f, 0, thickness); - GUI.DrawLine(spriteBatch, prevPos - new Vector2(0.0f, 1.0f), pos - new Vector2(0.0f, 1.0f), Color.Lime * 0.5f, 0, thickness); - } - - prevPos = pos; - } - - Rectangle sourceRect = heartrateFade.Bounds; - - Rectangle destinationRectangle = new Rectangle( - new Point((int)(currentHeartrateTime * targetRect.Width) + targetRect.Left - targetRect.Height, targetRect.Top), - new Point((int)(targetRect.Height * ((float)sourceRect.Width / (float)sourceRect.Height)), targetRect.Height)); - - if (destinationRectangle.Left < targetRect.Left) - { - Rectangle destinationRectangle2 = new Rectangle(); - destinationRectangle2.Location = new Point(targetRect.Right - (targetRect.Left - destinationRectangle.Left), targetRect.Top); - destinationRectangle2.Size = new Point(targetRect.Right - destinationRectangle2.Left, targetRect.Height); - - int originalWidth = sourceRect.Width; - sourceRect.Width = (int)(sourceRect.Width * ((float)(destinationRectangle.Right - targetRect.Left) / (float)targetRect.Height)); - sourceRect.X += originalWidth - sourceRect.Width; - - Rectangle sourceRect2 = heartrateFade.Bounds; - sourceRect2.Width -= sourceRect.Width; - spriteBatch.Draw(heartrateFade, destinationRectangle2, sourceRect2, Color.White); - - originalWidth = destinationRectangle.Width; - int newWidth = destinationRectangle.Right - targetRect.Left; - - destinationRectangle.Size = new Point(newWidth, targetRect.Height); - destinationRectangle.X += originalWidth - newWidth; - - GUI.DrawRectangle(spriteBatch, new Rectangle(destinationRectangle.Right, destinationRectangle.Top, - destinationRectangle2.Left - destinationRectangle.Right, destinationRectangle2.Height), Color.Black, true); - } - else - { - GUI.DrawRectangle(spriteBatch, new Rectangle(destinationRectangle.Right, destinationRectangle.Top, - targetRect.Right - destinationRectangle.Right, destinationRectangle.Height), Color.Black, true); - GUI.DrawRectangle(spriteBatch, new Rectangle(targetRect.Left, destinationRectangle.Top, - destinationRectangle.Left - targetRect.Left, destinationRectangle.Height), Color.Black, true); - } - - spriteBatch.Draw(heartrateFade, destinationRectangle, sourceRect, Color.White); - } - private void UpdateAfflictionInfos(IEnumerable afflictions) { foreach (Affliction affliction in afflictions) @@ -1634,12 +1379,6 @@ namespace Barotrauma var child = afflictionIconContainer.Content.FindChild(affliction); var afflictionStrengthBar = child.GetChildByUserData("afflictionstrength") as GUIProgressBar; afflictionStrengthBar.BarSize = affliction.Strength / affliction.Prefab.MaxStrength; - - if (afflictionInfoContainer.UserData == affliction) - { - UpdateAfflictionInfo(afflictionInfoContainer.Content, affliction); - } - if (afflictionTooltip != null && afflictionTooltip.UserData == affliction) { UpdateAfflictionInfo(afflictionTooltip.Content, affliction); @@ -1678,8 +1417,7 @@ namespace Barotrauma { //items can be dropped outside the health window if (!ignoreMousePos && - !healthWindow.Rect.Contains(PlayerInput.MousePosition) && - !afflictionInfoFrame.Rect.Contains(PlayerInput.MousePosition)) + !healthWindow.Rect.Contains(PlayerInput.MousePosition) ) { return false; } @@ -1701,35 +1439,6 @@ namespace Barotrauma return true; } - - private List GetAvailableMedicalItems() - { - List allInventoryItems = new List(); - allInventoryItems.AddRange(Character.Inventory.AllItems); - if (Character.SelectedCharacter?.Inventory != null && Character.CanAccessInventory(Character.SelectedCharacter.Inventory)) - { - allInventoryItems.AddRange(Character.SelectedCharacter.Inventory.AllItems); - } - if (Character.SelectedBy?.Inventory != null) - { - allInventoryItems.AddRange(Character.SelectedBy.Inventory.AllItems); - } - List medicalItems = new List(); - foreach (Item item in allInventoryItems) - { - foreach (Item containedItem in item.ContainedItems) - { - if (!containedItem.HasTag("medical") && !containedItem.HasTag("chem")) { continue; } - medicalItems.Add(containedItem); - } - - if (!item.HasTag("medical") && !item.HasTag("chem")) { continue; } - medicalItems.Add(item); - } - - return medicalItems.Distinct().ToList(); - } - private void UpdateLimbIndicators(float deltaTime, Rectangle drawArea) { if (!GameMain.Instance.Paused) @@ -1757,10 +1466,6 @@ namespace Barotrauma if (PlayerInput.PrimaryMouseButtonClicked() && highlightedLimbIndex > -1) { selectedLimbIndex = highlightedLimbIndex; - //afflictionContainer.ClearChildren(); - afflictionIconContainer.ClearChildren(); - afflictionInfoContainer.ClearChildren(); - afflictionInfoContainer.UserData = null; } } @@ -1931,14 +1636,14 @@ namespace Barotrauma i++; } - if (selectedLimbIndex > -1 && selectedLimbText != null) + if (selectedLimbIndex > -1 && afflictionIconContainer.Content.CountChildren > 0) { LimbHealth limbHealth = limbHealths[selectedLimbIndex]; if (limbHealth?.IndicatorSprite != null) { Rectangle selectedLimbArea = GetLimbHighlightArea(limbHealth, drawArea); GUI.DrawLine(spriteBatch, - new Vector2(selectedLimbText.Rect.X, selectedLimbText.Rect.Center.Y), + new Vector2(afflictionIconContainer.Rect.X, afflictionIconContainer.Rect.Y), selectedLimbArea.Center.ToVector2(), Color.LightGray * 0.5f, width: 4); } @@ -1954,7 +1659,7 @@ namespace Barotrauma private void DrawLimbAfflictionIcon(SpriteBatch spriteBatch, Affliction affliction, float iconScale, ref Vector2 iconPos) { - if (!affliction.ShouldShowIcon(Character)) { return; } + if (!affliction.ShouldShowIcon(Character) || affliction.Prefab.Icon == null) { return; } Vector2 iconSize = affliction.Prefab.Icon.size * iconScale; float showIconThreshold = Character.Controlled?.CharacterHealth == this ? affliction.Prefab.ShowIconThreshold : affliction.Prefab.ShowIconToOthersThreshold; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs index f4c76ac1f..c9c4d7300 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs @@ -632,7 +632,7 @@ namespace Barotrauma RefreshDeformations(); } - public void Draw(SpriteBatch spriteBatch, Camera cam, Color? overrideColor = null) + public void Draw(SpriteBatch spriteBatch, Camera cam, Color? overrideColor = null, bool disableDeformations = false) { float brightness = 1.0f - (burnOverLayStrength / 100.0f) * 0.5f; var spriteParams = Params.GetSprite(); @@ -678,7 +678,7 @@ namespace Barotrauma if (!hideLimb) { var deformSprite = DeformSprite; - if (deformSprite != null) + if (deformSprite != null && !disableDeformations) { if (ActiveDeformations.Any()) { @@ -999,10 +999,12 @@ namespace Barotrauma } float textureScale = wearable.InheritTextureScale ? TextureScale : wearable.Scale; + 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, -body.DrawRotation, + origin, rotation, Scale * textureScale, spriteEffect, depth); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index 69c8a1836..d8cb533ed 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -1320,11 +1320,13 @@ namespace Barotrauma continue; } + float avgOutCondition = (deconstructItem.OutConditionMin + deconstructItem.OutConditionMax) / 2; + int? deconstructProductPrice = targetItem.GetMinPrice(); if (deconstructProductPrice.HasValue) { if (!deconstructProductCost.HasValue) { deconstructProductCost = 0; } - deconstructProductCost += (int)(deconstructProductPrice * deconstructItem.OutCondition); + deconstructProductCost += (int)(deconstructProductPrice * avgOutCondition); } if (fabricationRecipe != null) @@ -1334,9 +1336,9 @@ namespace Barotrauma { NewMessage("Deconstructing \"" + itemPrefab.Name + "\" produces \"" + deconstructItem.ItemIdentifier + "\", which isn't required in the fabrication recipe of the item.", Color.Red); } - else if (ingredient.UseCondition && ingredient.MinCondition < deconstructItem.OutCondition) + else if (ingredient.UseCondition && ingredient.MinCondition < avgOutCondition) { - NewMessage($"Deconstructing \"{itemPrefab.Name}\" produces more \"{deconstructItem.ItemIdentifier}\", than what's required to fabricate the item (required: {targetItem.Name} {(int)(ingredient.MinCondition * 100)}%, output: {deconstructItem.ItemIdentifier} {(int)(deconstructItem.OutCondition * 100)}%)", Color.Red); + NewMessage($"Deconstructing \"{itemPrefab.Name}\" produces more \"{deconstructItem.ItemIdentifier}\", than what's required to fabricate the item (required: {targetItem.Name} {(int)(ingredient.MinCondition * 100)}%, output: {deconstructItem.ItemIdentifier} {(int)(avgOutCondition * 100)}%)", Color.Red); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs index ff2d3c4a7..2d0e44691 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs @@ -251,6 +251,11 @@ namespace Barotrauma private readonly bool isHorizontal; + /// + /// Setting this to true and CanBeFocused to false allows the list background to be unfocusable while the elements can still be interacted with. + /// + public bool CanInteractWhenUnfocusable { get; set; } = false; + /// For horizontal listbox, default side is on the bottom. For vertical, it's on the right. public GUIListBox(RectTransform rectT, bool isHorizontal = false, Color? color = null, string style = "", bool isScrollBarOnDefaultSide = true, bool useMouseDownToSelect = false) : base(style, rectT) { @@ -570,7 +575,7 @@ namespace Barotrauma if (child == null || !child.Visible) { continue; } // selecting - if (Enabled && CanBeFocused && child.CanBeFocused && child.Rect.Contains(PlayerInput.MousePosition) && GUI.IsMouseOn(child)) + if (Enabled && (CanBeFocused || CanInteractWhenUnfocusable) && child.CanBeFocused && child.Rect.Contains(PlayerInput.MousePosition) && GUI.IsMouseOn(child)) { child.State = ComponentState.Hover; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIProgressBar.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIProgressBar.cs index 0c373187d..33f42b55d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIProgressBar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIProgressBar.cs @@ -79,14 +79,14 @@ namespace Barotrauma new Rectangle( sliderArea.X + (int)sliceBorderSizes.X, sliderArea.Y, - (int)((sliderArea.Width - sliceBorderSizes.X - sliceBorderSizes.Z) * fillAmount), + (int)Math.Round((sliderArea.Width - sliceBorderSizes.X - sliceBorderSizes.Z) * fillAmount), sliderArea.Height) : new Rectangle( sliderArea.X, - (int)(sliderArea.Bottom - (sliderArea.Height - sliceBorderSizes.Y - sliceBorderSizes.W) * fillAmount - sliceBorderSizes.W), + (int)Math.Round(sliderArea.Bottom - (sliderArea.Height - sliceBorderSizes.Y - sliceBorderSizes.W) * fillAmount - sliceBorderSizes.W), sliderArea.Width, - (int)((sliderArea.Height - sliceBorderSizes.Y - sliceBorderSizes.W) * fillAmount)); + (int)Math.Round((sliderArea.Height - sliceBorderSizes.Y - sliceBorderSizes.W) * fillAmount)); sliderRect.Width = Math.Max(sliderRect.Width, 1); sliderRect.Height = Math.Max(sliderRect.Height, 1); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index 57c248466..70c0e9f76 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -1219,7 +1219,11 @@ namespace Barotrauma GUIFrame characterInfoFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.3f), talentInfoLayoutGroup.RectTransform, Anchor.TopLeft), style: null); GUILayoutGroup characterInfoColumn = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), characterInfoFrame.RectTransform, anchor: Anchor.TopLeft), childAnchor: Anchor.TopLeft, isHorizontal: true); - CreateCharacterSheet(characterInfoColumn); + // move to a different tab menu + if (GameSettings.VerboseLogging) + { + CreateCharacterSheet(characterInfoColumn); + } if (!TalentTree.JobTalentTrees.TryGetValue(controlledCharacter.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return; } @@ -1254,7 +1258,7 @@ namespace Barotrauma Stretch = true, }; - foreach (Talent talent in talentOption.Talents) + foreach (TalentPrefab talent in talentOption.Talents) { int optionPadding = GUI.IntScale(10); GUIFrame talentFrame = new GUIFrame(new RectTransform(new Point(talentOptionFrame.Rect.Width, talentOptionFrame.Rect.Height - optionPadding), talentOptionLayoutGroup.RectTransform), style: null) @@ -1269,13 +1273,13 @@ namespace Barotrauma GUIButton talentButton = new GUIButton(new RectTransform(Vector2.One, talentFrame.RectTransform, anchor: Anchor.Center), style: "TalentFrame") { - ToolTip = $"{TextManager.Get("talentname." + talent.Identifier, returnNull: true) ?? talent.Identifier} \n\n{TextManager.Get("talentdescription." + talent.Identifier, returnNull: true) ?? string.Empty}", + ToolTip = $"{talent.DisplayName}\n\n{talent.Description}", UserData = talent.Identifier, PressedColor = pressedColor, OnClicked = (button, userData) => { - talentTitleText.Text = TextManager.Get("talentname." + talent.Identifier, returnNull: true) ?? string.Empty; - talentDescriptionText.Text = TextManager.Get("talentdescription." + talent.Identifier, returnNull: true) ?? string.Empty; + talentTitleText.Text = talent.DisplayName; + talentDescriptionText.Text = talent.Description; // deselect other buttons in tier by removing their selected talents from pool foreach (GUIButton guiButton in talentOptionLayoutGroup.GetAllChildren()) @@ -1440,10 +1444,10 @@ namespace Barotrauma private readonly StatTypes[] combatStats = new StatTypes[] { - StatTypes.MaximumHealthMultiplier, - StatTypes.MovementSpeed, - StatTypes.SwimmingSpeed, - StatTypes.RepairSpeed, + StatTypes.MeleeAttackMultiplier, + StatTypes.MeleeAttackSpeed, + StatTypes.RangedAttackSpeed, + StatTypes.TurretAttackSpeed, }; private readonly StatTypes[] miscStats = new StatTypes[] diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index ac4b7ed69..520e873f5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -99,7 +99,9 @@ namespace Barotrauma crewList = new GUIListBox(new RectTransform(Vector2.One, crewArea.RectTransform), style: null, isScrollBarOnDefaultSide: false) { AutoHideScrollBar = false, + CanBeFocused = false, CanDragElements = true, + CanInteractWhenUnfocusable = true, OnSelected = (component, userData) => false, SelectMultiple = false, Spacing = (int)(GUI.Scale * 10), @@ -770,7 +772,7 @@ namespace Barotrauma if (IsSinglePlayer) { character.SetOrder(order, option, priority, orderGiver, speak: orderGiver != character); - string message = order?.GetChatMessage(character.Name, orderGiver.CurrentHull?.DisplayName, givingOrderToSelf: character == orderGiver, orderOption: option, priority: priority); + string message = order?.GetChatMessage(character.Name, orderGiver?.CurrentHull?.DisplayName, givingOrderToSelf: character == orderGiver, orderOption: option, priority: priority); orderGiver?.Speak(message); } else if (orderGiver != null) @@ -1905,13 +1907,23 @@ namespace Barotrauma } } - private bool CanSomeoneHearCharacter() + private bool CanCharacterBeHeard() { #if DEBUG if (Character.Controlled == null) { return true; } #endif - return Character.Controlled != null && - (characters.Any(c => c != Character.Controlled && c.CanHearCharacter(Character.Controlled)) || GetOrderableFriendlyNPCs().Any(c => c != Character.Controlled && c.CanHearCharacter(Character.Controlled))); + if (Character.Controlled != null) + { + if (characterContext == null) + { + return characters.Any(c => c != Character.Controlled && c.CanHearCharacter(Character.Controlled)) || GetOrderableFriendlyNPCs().Any(c => c != Character.Controlled && c.CanHearCharacter(Character.Controlled)); + } + else + { + return characterContext.CanHearCharacter(Character.Controlled); + } + } + return false; } private Entity FindEntityContext() @@ -2580,7 +2592,7 @@ namespace Barotrauma for (int i = 0; i < orders.Count; i++) { order = orders[i]; - disableNode = !CanSomeoneHearCharacter() || + disableNode = !CanCharacterBeHeard() || (order.MustSetTarget && (order.ItemComponentType != null || order.TargetItems.Length > 0) && order.GetMatchingItems(true, interactableFor: characterContext ?? Character.Controlled).None()); optionNodes.Add(new Tuple( @@ -2728,7 +2740,7 @@ namespace Barotrauma } var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, nodeDistance, contextualOrders.Count, MathHelper.ToRadians(90f + 180f / contextualOrders.Count)); - bool disableNode = !CanSomeoneHearCharacter(); + bool disableNode = !CanCharacterBeHeard(); for (int i = 0; i < contextualOrders.Count; i++) { optionNodes.Add(new Tuple( @@ -2766,7 +2778,7 @@ namespace Barotrauma if (checkIfOrderCanBeHeard && !disableNode) { - disableNode = !CanSomeoneHearCharacter(); + disableNode = !CanCharacterBeHeard(); } var mustSetOptionOrTarget = order.HasOptions; @@ -2827,7 +2839,7 @@ namespace Barotrauma if (disableNode) { node.CanBeFocused = icon.CanBeFocused = false; - CreateBlockIcon(node.RectTransform, tooltip: TextManager.Get("nocharactercanhear")); + CreateBlockIcon(node.RectTransform, tooltip: TextManager.Get(characterContext == null ? "nocharactercanhear" : "thischaractercanthear")); } else if (hotkey >= 0) { @@ -2999,11 +3011,11 @@ namespace Barotrauma "\n" + (!PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.leftmouse") : TextManager.Get("input.rightmouse")) + ": " + TextManager.Get("commandui.quickassigntooltip") + "\n" + (!PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.rightmouse") : TextManager.Get("input.leftmouse")) + ": " + TextManager.Get("commandui.manualassigntooltip")); } - if (!CanSomeoneHearCharacter()) + if (!CanCharacterBeHeard()) { node.CanBeFocused = false; if (icon != null) { icon.CanBeFocused = false; } - CreateBlockIcon(node.RectTransform, tooltip: TextManager.Get("nocharactercanhear")); + CreateBlockIcon(node.RectTransform, tooltip: TextManager.Get(characterContext == null ? "nocharactercanhear" : "thischaractercanthear")); } else if (hotkey >= 0) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index 953594247..919a36ab8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -174,6 +174,12 @@ namespace Barotrauma (int)SlotPositions[i].X, (int)SlotPositions[i].Y, (int)(slotSprite.size.X * multiplier), (int)(slotSprite.size.Y * multiplier)); + + if (SlotTypes[i] == InvSlotType.HealthInterface && + character.CharacterHealth?.InventorySlotContainer != null) + { + slotRect.Width = slotRect.Height = (int)(character.CharacterHealth.InventorySlotContainer.Rect.Width * 1.2f); + } ItemContainer itemContainer = slots[i].FirstOrDefault()?.GetComponent(); if (itemContainer != null) @@ -238,6 +244,8 @@ namespace Barotrauma { if (visualSlots[i].Disabled || (slots[i].HideIfEmpty && slots[i].Empty())) { return true; } + if (CharacterHealth.OpenHealthWindow != Character.Controlled?.CharacterHealth && SlotTypes[i] == InvSlotType.HealthInterface) { return true; } + if (layout == Layout.Default) { if (PersonalSlots.HasFlag(SlotTypes[i]) && !personalSlotArea.Contains(visualSlots[i].Rect.Center + visualSlots[i].DrawOffset.ToPoint())) { return true; } @@ -359,7 +367,7 @@ namespace Barotrauma int personalSlotX = HUDLayoutSettings.InventoryAreaLower.Right - SlotSize.X - Spacing; for (int i = 0; i < visualSlots.Length; i++) { - if (HideSlot(i)) { continue; } + if (HideSlot(i) || SlotTypes[i] == InvSlotType.HealthInterface) { continue; } if (SlotTypes[i] == InvSlotType.RightHand || SlotTypes[i] == InvSlotType.LeftHand) { continue; } if (PersonalSlots.HasFlag(SlotTypes[i])) { @@ -383,7 +391,7 @@ namespace Barotrauma continue; } - if (HideSlot(i)) { continue; } + if (HideSlot(i) || SlotTypes[i] == InvSlotType.HealthInterface) { continue; } if (PersonalSlots.HasFlag(SlotTypes[i])) { SlotPositions[i] = new Vector2(personalSlotX, personalSlotY); @@ -399,7 +407,7 @@ namespace Barotrauma x = lowerX; for (int i = 0; i < SlotPositions.Length; i++) { - if (!HideSlot(i)) { continue; } + if (!HideSlot(i) || SlotTypes[i] == InvSlotType.HealthInterface) { continue; } if (SlotTypes[i] == InvSlotType.RightHand || SlotTypes[i] == InvSlotType.LeftHand) { continue; } x -= visualSlots[i].Rect.Width + Spacing; SlotPositions[i] = new Vector2(x, GameMain.GraphicsHeight - bottomOffset); @@ -414,7 +422,7 @@ namespace Barotrauma for (int i = 0; i < SlotPositions.Length; i++) { - if (HideSlot(i)) { continue; } + if (HideSlot(i) || SlotTypes[i] == InvSlotType.HealthInterface) { continue; } if (SlotTypes[i] == InvSlotType.RightHand || SlotTypes[i] == InvSlotType.LeftHand) { continue; } if (PersonalSlots.HasFlag(SlotTypes[i])) { @@ -436,7 +444,7 @@ namespace Barotrauma SlotPositions[i] = new Vector2(rightSlot ? handSlotX : handSlotX - visualSlots[0].Rect.Width - Spacing, personalSlotY); continue; } - if (!HideSlot(i)) { continue; } + if (!HideSlot(i) || SlotTypes[i] == InvSlotType.HealthInterface) { continue; } SlotPositions[i] = new Vector2(x, GameMain.GraphicsHeight - bottomOffset); x += visualSlots[i].Rect.Width + Spacing; } @@ -450,7 +458,7 @@ namespace Barotrauma int x = startX, y = startY; for (int i = 0; i < SlotPositions.Length; i++) { - if (HideSlot(i)) continue; + if (HideSlot(i) || SlotTypes[i] == InvSlotType.HealthInterface) { continue; } if (SlotTypes[i] == InvSlotType.Card || SlotTypes[i] == InvSlotType.Headset || SlotTypes[i] == InvSlotType.InnerClothes) { SlotPositions[i] = new Vector2(x, y); @@ -462,7 +470,7 @@ namespace Barotrauma int n = 0; for (int i = 0; i < SlotPositions.Length; i++) { - if (HideSlot(i)) continue; + if (HideSlot(i) || SlotTypes[i] == InvSlotType.HealthInterface) { continue; } if (SlotTypes[i] != InvSlotType.Card && SlotTypes[i] != InvSlotType.Headset && SlotTypes[i] != InvSlotType.InnerClothes) { SlotPositions[i] = new Vector2(x, y); @@ -479,13 +487,23 @@ namespace Barotrauma } break; } - + + if (character.CharacterHealth?.UseHealthWindow ?? false) + { + Vector2 pos = character.CharacterHealth.InventorySlotContainer.Rect.Location.ToVector2(); + for (int i = 0; i < capacity; i++) + { + if (SlotTypes[i] != InvSlotType.HealthInterface) { continue; } + SlotPositions[i] = pos; + pos.Y += visualSlots[i].Rect.Height + Spacing; + } + } + CreateSlots(); if (layout == Layout.Default) { HUDLayoutSettings.InventoryTopY = visualSlots[0].EquipButtonRect.Y - (int)(15 * GUI.Scale); } - } protected override void ControlInput(Camera cam) @@ -679,7 +697,7 @@ namespace Barotrauma if (item != null) { var slot = visualSlots[i]; - if (item.AllowedSlots.Any(a => a != InvSlotType.Any)) + if (item.AllowedSlots.Any(a => a != InvSlotType.Any && a != InvSlotType.HealthInterface)) { HandleButtonEquipStates(item, slot, deltaTime); } @@ -858,7 +876,9 @@ namespace Barotrauma private QuickUseAction GetQuickUseAction(Item item, bool allowEquip, bool allowInventorySwap, bool allowApplyTreatment) { - if (allowApplyTreatment && CharacterHealth.OpenHealthWindow != null) + if (allowApplyTreatment && CharacterHealth.OpenHealthWindow != null && + //if the item can be equipped in the health interface slot, don't use it as a treatment but try to equip it + !item.AllowedSlots.Contains(InvSlotType.HealthInterface)) { return QuickUseAction.UseTreatment; } @@ -1153,7 +1173,7 @@ namespace Barotrauma for (int i = 0; i < capacity; i++) { - if (HideSlot(i)) { continue; } + if (HideSlot(i) || SlotTypes[i] == InvSlotType.HealthInterface) { continue; } //don't draw the item if it's being dragged out of the slot bool drawItem = !DraggingItems.Any() || !slots[i].Items.All(it => DraggingItems.Contains(it)) || visualSlots[i].MouseOn(); @@ -1207,7 +1227,7 @@ namespace Barotrauma highlightedQuickUseSlot = visualSlots[i]; } - if (!slots[i].First().AllowedSlots.Any(a => a == InvSlotType.Any)) + if (!slots[i].First().AllowedSlots.Any(a => a == InvSlotType.Any) || SlotTypes[i] == InvSlotType.HealthInterface) { continue; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/GeneticMaterial.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/GeneticMaterial.cs new file mode 100644 index 000000000..981c388a2 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/GeneticMaterial.cs @@ -0,0 +1,74 @@ +using Barotrauma.Networking; +using Microsoft.Xna.Framework; +using System.Linq; + +namespace Barotrauma.Items.Components +{ + partial class GeneticMaterial : ItemComponent + { + [Serialize(0.0f, false)] + public float TooltipValueMin { get; set; } + + [Serialize(0.0f, false)] + public float TooltipValueMax { get; set; } + + public override void AddTooltipInfo(ref string name, ref string description) + { + if (!string.IsNullOrEmpty(materialName)) + { + string mergedMaterialName = materialName; + foreach (Item containedItem in item.ContainedItems) + { + var containedMaterial = containedItem.GetComponent(); + if (containedMaterial == null) { continue; } + mergedMaterialName += ", " + containedMaterial.materialName; + } + name = TextManager.GetWithVariable("entityname.geneticmaterial", "[type]", mergedMaterialName); + } + + if (Tainted) + { + name = TextManager.GetWithVariable("entityname.taintedgeneticmaterial", "[geneticmaterialname]", name); + } + + if (TextManager.ContainsTag("entitydescription."+Item.prefab.Identifier)) + { + int value = (int)MathHelper.Lerp(TooltipValueMin, TooltipValueMax, item.ConditionPercentage / 100.0f); + description = TextManager.GetWithVariable("entitydescription." + Item.prefab.Identifier, "[value]", value.ToString()); + } + } + + public void ModifyDeconstructInfo(Deconstructor deconstructor, ref string buttonText, ref string infoText) + { + if (deconstructor.InputContainer.Inventory.AllItems.Count() == 2) + { + if (!deconstructor.InputContainer.Inventory.AllItems.All(it => it.prefab == item.prefab)) + { + buttonText = TextManager.Get("researchstation.combine"); + infoText = TextManager.Get("researchstation.combine.infotext"); + } + else + { + buttonText = TextManager.Get("researchstation.refine"); + int taintedProbability = (int)(GetTaintedProbabilityOnRefine(Character.Controlled) * 100); + infoText = TextManager.GetWithVariable("researchstation.refine.infotext", "[taintedprobability]", taintedProbability.ToString()); + } + } + } + + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + { + Tainted = msg.ReadBoolean(); + if (Tainted) + { + uint selectedTaintedEffectId = msg.ReadUInt32(); + selectedTaintedEffect = AfflictionPrefab.Prefabs.Find(a => a.UIntIdentifier == selectedTaintedEffectId); + } + else + { + uint selectedEffectId = msg.ReadUInt32(); + selectedEffect = AfflictionPrefab.Prefabs.Find(a => a.UIntIdentifier == selectedEffectId); + } + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index faaa76c27..5e5a9357a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -621,6 +621,6 @@ namespace Barotrauma.Items.Components } OnResolutionChanged(); } - public virtual void AddTooltipInfo(ref string description) { } + public virtual void AddTooltipInfo(ref string name, ref string description) { } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs index 4da1bb77e..4a1049947 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemContainer.cs @@ -58,6 +58,9 @@ namespace Barotrauma.Items.Components [Serialize(null, false)] public string ContainedStateIndicatorStyle { get; set; } + [Serialize(-1, false, description: "Can be used to make the contained state indicator display the condition of the item in a specific slot even when the container's capacity is more than 1.")] + public int ContainedStateIndicatorSlot { get; set; } + [Serialize(true, false, description: "Should an indicator displaying the state of the contained items be displayed on this item's inventory slot. "+ "If this item can only contain one item, the indicator will display the condition of the contained item, otherwise it will indicate how full the item is.")] public bool ShowContainedStateIndicator { get; set; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs index 24149ae69..62c485a67 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs @@ -14,12 +14,19 @@ namespace Barotrauma.Items.Components } private GUIButton activateButton; private GUIComponent inputInventoryHolder, outputInventoryHolder; - private GUICustomComponent inputInventoryOverlay; private GUIComponent inSufficientPowerWarning; private bool pendingState; + private GUITextBlock infoArea; + + [Serialize("DeconstructorDeconstruct", true)] + public string ActivateButtonText { get; set; } + + [Serialize(0.0f, true)] + public float InfoAreaWidth { get; set; } + partial void InitProjSpecific(XElement element) { CreateGUI(); @@ -39,6 +46,12 @@ namespace Barotrauma.Items.Components RelativeSpacing = 0.08f }; + new GUITextBlock(new RectTransform(new Vector2(1f, 0.07f), paddedFrame.RectTransform), item.Name, font: GUI.SubHeadingFont) + { + TextAlignment = Alignment.Center, + AutoScaleHorizontal = true + }; + var topFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.5f), paddedFrame.RectTransform), style: null); // === INPUT LABEL === // @@ -55,22 +68,23 @@ namespace Barotrauma.Items.Components // === INPUT SLOTS === // inputInventoryHolder = new GUIFrame(new RectTransform(new Vector2(0.7f, 1f), inputArea.RectTransform), style: null); - inputInventoryOverlay = new GUICustomComponent(new RectTransform(Vector2.One, inputInventoryHolder.RectTransform), DrawOverLay, null) { CanBeFocused = false }; + new GUICustomComponent(new RectTransform(Vector2.One, inputInventoryHolder.RectTransform), DrawOverLay, null) { CanBeFocused = false }; // === ACTIVATE BUTTON === // - var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.4f, 0.7f), inputArea.RectTransform), childAnchor: Anchor.CenterLeft); + var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.4f, 0.8f), inputArea.RectTransform), childAnchor: Anchor.CenterLeft); activateButton = new GUIButton(new RectTransform(new Vector2(0.95f, 0.8f), buttonContainer.RectTransform), TextManager.Get("DeconstructorDeconstruct"), style: "DeviceButton") { TextBlock = { AutoScaleHorizontal = true }, OnClicked = ToggleActive }; inSufficientPowerWarning = new GUITextBlock(new RectTransform(Vector2.One, activateButton.RectTransform), - TextManager.Get("DeconstructorNoPower"), textColor: GUI.Style.Orange, textAlignment: Alignment.Center, color: Color.Black, style: "OuterGlow") + TextManager.Get("DeconstructorNoPower"), textColor: GUI.Style.Orange, textAlignment: Alignment.Center, color: Color.Black, style: "OuterGlow", wrap: true) { HoverColor = Color.Black, IgnoreLayoutGroups = true, Visible = false, - CanBeFocused = false + CanBeFocused = false, + AutoScaleHorizontal = true }; // === OUTPUT AREA === // @@ -86,8 +100,62 @@ namespace Barotrauma.Items.Components outputLabel.RectTransform.Resize(new Point((int) outputLabel.Font.MeasureString(outputLabel.Text).X, outputLabel.RectTransform.Rect.Height)); new GUIFrame(new RectTransform(Vector2.One, outputLabelArea.RectTransform), style: "HorizontalLine"); + var outputArea = new GUILayoutGroup(new RectTransform(new Vector2(1f, 1f), bottomFrame.RectTransform, Anchor.CenterLeft), childAnchor: Anchor.BottomLeft, isHorizontal: true) { Stretch = true, RelativeSpacing = 0.05f }; + // === OUTPUT SLOTS === // - outputInventoryHolder = new GUIFrame(new RectTransform(new Vector2(1f, 1f), bottomFrame.RectTransform, Anchor.CenterLeft), style: null); + outputInventoryHolder = new GUIFrame(new RectTransform(new Vector2(1f - InfoAreaWidth, 1f), outputArea.RectTransform, Anchor.CenterLeft), style: null); + + if (InfoAreaWidth >= 0.0f) + { + var infoAreaContainer = new GUILayoutGroup(new RectTransform(new Vector2(InfoAreaWidth, 0.8f), outputArea.RectTransform), childAnchor: Anchor.CenterLeft); + infoArea = new GUITextBlock(new RectTransform(new Vector2(0.95f, 0.95f), infoAreaContainer.RectTransform), string.Empty, wrap: true); + } + + ActivateButton.OnAddedToGUIUpdateList += (GUIComponent component) => + { + activateButton.Enabled = true; + infoArea.Text = string.Empty; + if (IsActive) + { + activateButton.Text = TextManager.Get("DeconstructorCancel"); + return; + } + bool outputsFound = false; + foreach (var (inputItem, deconstructItem) in GetAvailableOutputs(checkRequiredOtherItems: true)) + { + outputsFound = true; + if (!string.IsNullOrEmpty(deconstructItem.ActivateButtonText)) + { + string buttonText = TextManager.Get(deconstructItem.ActivateButtonText, returnNull: true) ?? deconstructItem.ActivateButtonText; + string infoText = string.Empty; + if (!string.IsNullOrEmpty(deconstructItem.InfoText)) + { + infoText = TextManager.Get(deconstructItem.InfoText, returnNull: true) ?? deconstructItem.InfoText; + } + inputItem.GetComponent()?.ModifyDeconstructInfo(this, ref buttonText, ref infoText); + activateButton.Text = buttonText; + if (infoArea != null) + { + infoArea.Text = infoText; + } + return; + } + } + //no valid outputs found: check if we're missing some required items from the input slots and display a message about it if possible + if (!outputsFound && infoArea != null) + { + foreach (var (inputItem, deconstructItem) in GetAvailableOutputs(checkRequiredOtherItems: false)) + { + if (deconstructItem.RequiredOtherItem.Any() && !string.IsNullOrEmpty(deconstructItem.InfoTextOnOtherItemMissing)) + { + string missingItemName = TextManager.Get("entityname." + deconstructItem.RequiredOtherItem.First(), returnNull: true); + infoArea.Text = TextManager.GetWithVariable(deconstructItem.InfoTextOnOtherItemMissing, "[itemname]", missingItemName); + } + } + } + activateButton.Enabled = inputContainer.Inventory.AllItems.Any(); + activateButton.Text = TextManager.Get(ActivateButtonText); + }; } public override bool Select(Character character) @@ -126,14 +194,30 @@ namespace Barotrauma.Items.Components private void DrawOverLay(SpriteBatch spriteBatch, GUICustomComponent overlayComponent) { overlayComponent.RectTransform.SetAsLastChild(); - if (!(inputContainer?.Inventory?.visualSlots is { } visualSlots)) { return; } - var lastSlot = visualSlots.Last(); - GUI.DrawRectangle(spriteBatch, - new Rectangle( - lastSlot.Rect.X, lastSlot.Rect.Y + (int)(lastSlot.Rect.Height * (1.0f - progressState)), - lastSlot.Rect.Width, (int)(lastSlot.Rect.Height * progressState)), - GUI.Style.Green * 0.5f, isFilled: true); + if (!(inputContainer?.Inventory?.visualSlots is { } visualSlots)) { return; } + + if (DeconstructItemsSimultaneously) + { + for (int i = 0; i < InputContainer.Inventory.Capacity; i++) + { + if (InputContainer.Inventory.GetItemAt(i) == null) { continue; } + DrawProgressBar(InputContainer.Inventory.visualSlots[i]); + } + } + else + { + DrawProgressBar(inputContainer.Inventory.visualSlots.Last()); + } + + void DrawProgressBar(VisualSlot slot) + { + GUI.DrawRectangle(spriteBatch, + new Rectangle( + slot.Rect.X, slot.Rect.Y + (int)(slot.Rect.Height * (1.0f - progressState)), + slot.Rect.Width, (int)(slot.Rect.Height * progressState)), + GUI.Style.Green * 0.5f, isFilled: true); + } } public override void UpdateHUD(Character character, float deltaTime, Camera cam) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index 0e9e7b897..b0aee591b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -403,7 +403,7 @@ namespace Barotrauma.Items.Components scissorComponent = new GUIScissorComponent(new RectTransform(Vector2.One, submarineContainer.RectTransform, Anchor.Center)); miniMapContainer = new GUIFrame(new RectTransform(Vector2.One, scissorComponent.Content.RectTransform, Anchor.Center), style: null) { CanBeFocused = false }; - miniMapFrame = CreateMiniMap(item.Submarine, miniMapContainer, MiniMapSettings.Default, null, out hullStatusComponents); + miniMapFrame = CreateMiniMap(item.Submarine, submarineContainer, MiniMapSettings.Default, null, out hullStatusComponents); IEnumerable pointsOfInterest = Item.ItemList.Where(it => it.Submarine == item.Submarine && !it.HiddenInGame && !it.NonInteractable && it.GetComponent() != null); electricalFrame = CreateMiniMap(item.Submarine, miniMapContainer, new MiniMapSettings(createHullElements: false), pointsOfInterest, out electricalMapComponents); @@ -458,7 +458,7 @@ namespace Barotrauma.Items.Components CreateHUD(); } - if (PlayerInput.PrimaryMouseButtonDown()) + if (PlayerInput.PrimaryMouseButtonDown() && currentMode != MiniMapMode.HullStatus) { if (GUI.MouseOn == scissorComponent || scissorComponent.IsParentOf(GUI.MouseOn)) { @@ -466,20 +466,16 @@ namespace Barotrauma.Items.Components } } - float newZoom = Zoom; - if (Math.Abs(PlayerInput.ScrollWheelSpeed) > 0 && (GUI.MouseOn == scissorComponent || scissorComponent.IsParentOf(GUI.MouseOn))) + if (currentMode != MiniMapMode.HullStatus && Math.Abs(PlayerInput.ScrollWheelSpeed) > 0 && (GUI.MouseOn == scissorComponent || scissorComponent.IsParentOf(GUI.MouseOn))) { - newZoom = Math.Clamp(Zoom + PlayerInput.ScrollWheelSpeed / 1000.0f * Zoom, minZoom, maxZoom); + float newZoom = Math.Clamp(Zoom + PlayerInput.ScrollWheelSpeed / 1000.0f * Zoom, minZoom, maxZoom); float distanceScale = newZoom / Zoom; mapOffset *= distanceScale; + recalculate |= !MathUtils.NearlyEqual(Zoom, newZoom); + Zoom = newZoom; } - recalculate |= !MathUtils.NearlyEqual(Zoom, newZoom); - Zoom = newZoom; - - Vector2 elementScale = new Vector2(Zoom); - if (dragMapStart is { } dragStart) { if (dragMap || Vector2.DistanceSquared(dragStart, PlayerInput.MousePosition) > GUI.IntScale(dragTreshold * dragTreshold)) @@ -487,16 +483,11 @@ namespace Barotrauma.Items.Components mapOffset.X += PlayerInput.MouseSpeed.X; mapOffset.Y += PlayerInput.MouseSpeed.Y; - recalculate |= PlayerInput.MouseSpeed != Vector2.Zero; + recalculate = true; dragMap = true; } } - var (maxWidth, maxHeight) = miniMapContainer.Rect.Size.ToVector2() / 2f / Zoom; - - mapOffset.X = Math.Clamp(mapOffset.X, -maxWidth, maxWidth); - mapOffset.Y = Math.Clamp(mapOffset.Y, -maxHeight, maxHeight); - if (!PlayerInput.PrimaryMouseButtonHeld()) { dragMapStart = null; @@ -505,10 +496,15 @@ namespace Barotrauma.Items.Components if (recalculate) { - miniMapContainer.RectTransform.LocalScale = elementScale; + miniMapContainer.RectTransform.LocalScale = new Vector2(Zoom); miniMapContainer.RectTransform.RecalculateChildren(true, true); miniMapContainer.RectTransform.AbsoluteOffset = mapOffset.ToPoint(); recalculate = false; + + var (maxWidth, maxHeight) = miniMapContainer.Rect.Size.ToVector2() / 2f / Zoom; + + mapOffset.X = Math.Clamp(mapOffset.X, -maxWidth, maxWidth); + mapOffset.Y = Math.Clamp(mapOffset.Y, -maxHeight, maxHeight); } // is there a better way to do this? @@ -583,6 +579,7 @@ namespace Barotrauma.Items.Components private void DrawHUDFront(SpriteBatch spriteBatch, GUICustomComponent container) { // TODO remove + #warning remove if (currentMode == MiniMapMode.HullCondition) { const string wipText = "work in progress"; @@ -757,7 +754,7 @@ namespace Barotrauma.Items.Components foreach (Item foundItem in foundItems) { RelativeEntityRect scaledRect = new RelativeEntityRect(dockedBorders, foundItem.WorldRect); - Vector2 pos = (scaledRect.PositionRelativeTo(parentRect, skipOffset: true) + scaledRect.SizeRelativeTo(parentRect) / 2f) / Zoom; + Vector2 pos = scaledRect.PositionRelativeTo(parentRect, skipOffset: true) + scaledRect.SizeRelativeTo(parentRect) / 2f; positions.Add(pos); } @@ -873,10 +870,14 @@ namespace Barotrauma.Items.Components string line1 = gapOpenSum > 0.1f ? TextManager.Get("MiniMapHullBreach") : string.Empty; Color line1Color = GUI.Style.Red; - string line2 = oxygenAmount == null ? TextManager.Get("MiniMapAirQualityUnavailable") : TextManager.AddPunctuation(':', TextManager.Get("MiniMapAirQuality"), +(int)oxygenAmount + " %"); + string line2 = oxygenAmount == null ? + TextManager.Get("MiniMapAirQualityUnavailable") : + TextManager.AddPunctuation(':', TextManager.Get("MiniMapAirQuality"), (int)Math.Round(oxygenAmount.Value) + "%"); Color line2Color = oxygenAmount == null ? GUI.Style.Red : Color.Lerp(GUI.Style.Red, Color.LightGreen, (float)oxygenAmount / 100.0f); - string line3 = waterAmount == null ? TextManager.Get("MiniMapWaterLevelUnavailable") : TextManager.AddPunctuation(':', TextManager.Get("MiniMapWaterLevel"), (int)(waterAmount * 100.0f) + " %"); + string line3 = waterAmount == null ? + TextManager.Get("MiniMapWaterLevelUnavailable") : + TextManager.AddPunctuation(':', TextManager.Get("MiniMapWaterLevel"), (int)Math.Round(waterAmount.Value * 100.0f) + "%"); Color line3Color = waterAmount == null ? GUI.Style.Red : Color.Lerp(Color.LightGreen, GUI.Style.Red, (float)waterAmount); SetTooltip(borderComponent.Rect.Center, header, line1, line2, line3, line1Color, line2Color, line3Color); @@ -958,7 +959,7 @@ namespace Barotrauma.Items.Components Rectangle parentRect = container.Rect; if (miniMapFrame is { } miniMap) { parentRect = miniMap.Rect; } - DrawSubmarine(spriteBatch, parentRect); + DrawSubmarine(spriteBatch); } if (Voltage < MinVoltage) { return; } @@ -979,7 +980,7 @@ namespace Barotrauma.Items.Components Vector2 spriteScale = targetSize / pingCircle.size; float scale = Math.Min(blipState, maxBlipState / 2f); float alpha = 1.0f - Math.Clamp((blipState - maxBlipState * 0.25f) * 2f, 0f, 1f); - pingCircle.Draw(spriteBatch, miniMapFrame.Rect.Location.ToVector2() + (blip * Zoom), GUI.Style.Red * alpha, pingCircle.Origin, 0f, spriteScale * scale, SpriteEffects.None); + pingCircle.Draw(spriteBatch, electricalFrame.Rect.Location.ToVector2() + blip * Zoom, GUI.Style.Red * alpha, pingCircle.Origin, 0f, spriteScale * scale, SpriteEffects.None); } } } @@ -1002,8 +1003,15 @@ namespace Barotrauma.Items.Components { RectangleF waterRect = new RectangleF(hullFrame.Rect.X, hullFrame.Rect.Y + hullFrame.Rect.Height * (1.0f - waterAmount), hullFrame.Rect.Width, hullFrame.Rect.Height * waterAmount); + const float width = 1f; + GUI.DrawFilledRectangle(spriteBatch, waterRect, HullWaterColor); - GUI.DrawLine(spriteBatch, waterRect.Location, new Vector2(waterRect.Right, waterRect.Y), HullWaterLineColor); + + if (!MathUtils.NearlyEqual(waterAmount, 1.0f)) + { + Vector2 offset = new Vector2(0, width); + GUI.DrawLine(spriteBatch, waterRect.Location + offset, new Vector2(waterRect.Right, waterRect.Y) + offset, HullWaterLineColor, width: width); + } } } @@ -1079,7 +1087,7 @@ namespace Barotrauma.Items.Components submarinePreview = rt; } - private void DrawSubmarine(SpriteBatch spriteBatch, Rectangle parentRect) + private void DrawSubmarine(SpriteBatch spriteBatch) { Rectangle prevScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle; spriteBatch.End(); @@ -1094,7 +1102,8 @@ namespace Barotrauma.Items.Components Color blueprintBlue = BlueprintBlue * currentMode switch { MiniMapMode.HullStatus => 0.1f, MiniMapMode.ElectricalView => 0.1f, _ => 0.5f }; Vector2 origin = new Vector2(texture.Width / 2f, texture.Height / 2f); - spriteBatch.Draw(texture, parentRect.Center.ToVector2(), null, blueprintBlue, 0f, origin, Zoom, SpriteEffects.None, 0f); + float scale = currentMode == MiniMapMode.HullStatus ? 1.0f : Zoom; + spriteBatch.Draw(texture, miniMapContainer.Center, null, blueprintBlue, 0f, origin, scale, SpriteEffects.None, 0f); spriteBatch.End(); } @@ -1281,12 +1290,12 @@ namespace Barotrauma.Items.Components GUIFrame hullContainer = new GUIFrame(new RectTransform(containerScale * elementPadding, parent.RectTransform, Anchor.Center), style: null); ImmutableHashSet connectedSubs = sub.GetConnectedSubs().ToImmutableHashSet(); - ImmutableHashSet hullList = ImmutableHashSet.Empty; - ImmutableDictionary> combinedHulls = ImmutableDictionary>.Empty; + ImmutableArray hullList = ImmutableArray.Empty; + ImmutableDictionary> combinedHulls = ImmutableDictionary>.Empty; if (settings.CreateHullElements) { - hullList = Hull.hullList.Where(IsPartofSub).ToImmutableHashSet(); + hullList = Hull.hullList.Where(IsPartofSub).ToImmutableArray(); combinedHulls = CombinedHulls(hullList); } @@ -1436,7 +1445,7 @@ namespace Barotrauma.Items.Components } } - private static ImmutableDictionary> CombinedHulls(ImmutableHashSet hulls) + private static ImmutableDictionary> CombinedHulls(ImmutableArray hulls) { Dictionary> combinedHulls = new Dictionary>(); @@ -1460,10 +1469,10 @@ namespace Barotrauma.Items.Components } } - return combinedHulls.ToImmutableDictionary(pair => pair.Key, pair => pair.Value.ToImmutableHashSet()); + return combinedHulls.ToImmutableDictionary(pair => pair.Key, pair => pair.Value.ToImmutableArray()); } - private static MiniMapHullData ConstructHullPolygon(Hull mainHull, ImmutableHashSet linkedHulls, GUIComponent parent, RectangleF worldBorders) + private static MiniMapHullData ConstructHullPolygon(Hull mainHull, ImmutableArray linkedHulls, GUIComponent parent, RectangleF worldBorders) { Rectangle parentRect = parent.Rect; @@ -1500,6 +1509,8 @@ namespace Barotrauma.Items.Components hullRefs.Add(hull); } + hullRefs.Reverse(); // I have no idea why this is required + ImmutableArray snappedRectangles = ToolBox.SnapRectangles(normalizedRects, treshold: 1); List> polygon = ToolBox.CombineRectanglesIntoShape(snappedRectangles); @@ -1508,6 +1519,7 @@ namespace Barotrauma.Items.Components foreach (List list in polygon) { + // scale down the polygon just a tiny bit var (polySizeX, polySizeY) = ToolBox.GetPolygonBoundingBoxSize(list); float sizeX = polySizeX - 1f, sizeY = polySizeY - 1f; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RemoteController.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RemoteController.cs new file mode 100644 index 000000000..bf5405474 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/RemoteController.cs @@ -0,0 +1,22 @@ +using Microsoft.Xna.Framework.Graphics; + +namespace Barotrauma.Items.Components +{ + partial class RemoteController : ItemComponent + { + public override void DrawHUD(SpriteBatch spriteBatch, Character character) + { + currentTarget?.DrawHUD(spriteBatch, Screen.Selected.Cam, character); + } + + public override void UpdateHUD(Character character, float deltaTime, Camera cam) + { + currentTarget?.UpdateHUD(cam, character,deltaTime); + } + + public override void AddToGUIUpdateList() + { + currentTarget?.AddToGUIUpdateList(); + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs index 4efc832fc..336769480 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs @@ -54,6 +54,11 @@ namespace Barotrauma.Items.Components if (wireComponent != null) { equippedWire = wireComponent; + var connectedEnd = equippedWire.OtherConnection(null); + if (connectedEnd?.Item.Submarine != null && panel.Item.Submarine != connectedEnd.Item.Submarine) + { + equippedWire = null; + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs index fe20dca85..e13440e0e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs @@ -63,11 +63,11 @@ namespace Barotrauma.Items.Components } OutputValue = input; - ShowOnDisplay(input); + ShowOnDisplay(input, addToHistory: true); item.SendSignal(input, "signal_out"); } - partial void ShowOnDisplay(string input, bool addToHistory = true) + partial void ShowOnDisplay(string input, bool addToHistory) { if (addToHistory) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs index 04313fee8..f9a7b5e9a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs @@ -39,6 +39,34 @@ namespace Barotrauma.Items.Components private set; } + [Serialize(false, false)] + public bool SeeThroughWalls + { + get; + private set; + } + + [Serialize(true, false)] + public bool ShowDeadCharacters + { + get; + private set; + } + + [Serialize(true, false)] + public bool ShowTexts + { + get; + private set; + } + + [Serialize("72,119,72,120", false)] + public Color OverlayColor + { + get; + private set; + } + private readonly List visibleCharacters = new List(); private const float UpdateInterval = 0.5f; @@ -91,12 +119,13 @@ namespace Barotrauma.Items.Components foreach (Character c in Character.CharacterList) { if (c == equipper || !c.Enabled || c.Removed) { continue; } + if (!ShowDeadCharacters && c.IsDead) { continue; } float dist = Vector2.DistanceSquared(refEntity.WorldPosition, c.WorldPosition); if (dist < Range * Range) { Vector2 diff = c.WorldPosition - refEntity.WorldPosition; - if (Submarine.CheckVisibility(refEntity.SimPosition, refEntity.SimPosition + ConvertUnits.ToSimUnits(diff)) == null) + if (SeeThroughWalls || Submarine.CheckVisibility(refEntity.SimPosition, refEntity.SimPosition + ConvertUnits.ToSimUnits(diff)) == null) { visibleCharacters.Add(c); } @@ -123,27 +152,54 @@ namespace Barotrauma.Items.Components { if (character == null) { return; } - GUI.UIGlow.Draw(spriteBatch, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), - Color.LightGreen * 0.5f); - - Character closestCharacter = null; - float closestDist = float.PositiveInfinity; - foreach (Character c in visibleCharacters) + if (OverlayColor.A > 0) { - if (c == character || !c.Enabled || c.Removed) { continue; } - - float dist = Vector2.DistanceSquared(GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition), c.WorldPosition); - if (dist < closestDist) - { - closestCharacter = c; - closestDist = dist; - } + GUI.UIGlow.Draw(spriteBatch, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), OverlayColor); } - if (closestCharacter != null) + if (ShowTexts) { - float dist = Vector2.Distance(GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition), closestCharacter.WorldPosition); - DrawCharacterInfo(spriteBatch, closestCharacter, 1.0f - MathHelper.Max((dist - (Range - FadeOutRange)) / FadeOutRange, 0.0f)); + Character closestCharacter = null; + float closestDist = float.PositiveInfinity; + foreach (Character c in visibleCharacters) + { + if (c == character || !c.Enabled || c.Removed) { continue; } + + float dist = Vector2.DistanceSquared(GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition), c.WorldPosition); + if (dist < closestDist) + { + closestCharacter = c; + closestDist = dist; + } + } + + if (closestCharacter != null) + { + float dist = Vector2.Distance(GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition), closestCharacter.WorldPosition); + DrawCharacterInfo(spriteBatch, closestCharacter, 1.0f - MathHelper.Max((dist - (Range - FadeOutRange)) / FadeOutRange, 0.0f)); + } + } + + if (SeeThroughWalls) + { + spriteBatch.End(); + GameMain.LightManager.SolidColorEffect.Parameters["color"].SetValue(Color.Red.ToVector4() * (0.35f + (float)Math.Sin(Timing.TotalTime * 1.6f) * 0.05f)); + GameMain.LightManager.SolidColorEffect.CurrentTechnique = GameMain.LightManager.SolidColorEffect.Techniques["SolidColorBlur"]; + GameMain.LightManager.SolidColorEffect.Parameters["blurDistance"].SetValue(0.03f + (float)Math.Sin(Timing.TotalTime) * 0.01f); + GameMain.LightManager.SolidColorEffect.CurrentTechnique.Passes[0].Apply(); + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Additive, transformMatrix: Screen.Selected.Cam.Transform, effect: GameMain.LightManager.SolidColorEffect); + + foreach (Character c in visibleCharacters) + { + if (c == character || !c.Enabled || c.Removed) { continue; } + foreach (Limb limb in c.AnimController.Limbs) + { + limb.Draw(spriteBatch, Screen.Selected.Cam, disableDeformations: true); + } + } + + spriteBatch.End(); + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs index 9692249c8..b489954a0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Wearable.cs @@ -13,7 +13,7 @@ namespace Barotrauma.Items.Components description += $"\n ‖color:{colorStr}‖{roundedValue.ToString("-0;+#")}%‖color:end‖ {AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier.Equals(afflictionIdentifier, StringComparison.OrdinalIgnoreCase))?.Name ?? afflictionIdentifier}"; } - public override void AddTooltipInfo(ref string description) + public override void AddTooltipInfo(ref string name, ref string description) { if (damageModifiers.Any(d => !MathUtils.NearlyEqual(d.DamageMultiplier, 1f)) || SkillModifiers.Any()) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index 21ac893a5..a4cc3788d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -297,9 +297,10 @@ namespace Barotrauma } } + string name = item.Name; foreach (ItemComponent component in item.Components) { - component.AddTooltipInfo(ref description); + component.AddTooltipInfo(ref name, ref description); } if (item.Prefab.ShowContentsInTooltip && item.OwnInventory != null) @@ -315,7 +316,7 @@ namespace Barotrauma string colorStr = XMLExtensions.ColorToString(!item.AllowStealing ? GUI.Style.Red : Color.White); - toolTip = $"‖color:{colorStr}‖{item.Name}‖color:end‖"; + toolTip = $"‖color:{colorStr}‖{name}‖color:end‖"; if (itemsInSlot.All(it => it.NonInteractable || it.NonPlayerTeamInteractable)) { toolTip += " " + TextManager.Get("connectionlocked"); @@ -719,7 +720,7 @@ namespace Barotrauma spacing = new Vector2(10 * UIScale, (10 + UnequippedIndicator.size.Y) * UIScale); } - int columns = (int)Math.Max(Math.Floor(Math.Sqrt(itemCapacity)), 1); + int columns = MathHelper.Clamp((int)Math.Floor(Math.Sqrt(itemCapacity)), 1, container.SlotsPerRow); while (itemCapacity / columns * (subRect.Height + spacing.Y) > GameMain.GraphicsHeight * 0.5f) { columns++; @@ -1543,13 +1544,13 @@ namespace Barotrauma } else { - var containedItem = itemContainer.Inventory.slots[0].FirstOrDefault(); - containedState = itemContainer.Inventory.Capacity == 1 ? + var containedItem = itemContainer.Inventory.slots[Math.Max(itemContainer.ContainedStateIndicatorSlot, 0)].FirstOrDefault(); + containedState = itemContainer.Inventory.Capacity == 1 || itemContainer.ContainedStateIndicatorSlot > -1 ? (containedItem == null ? 0.0f : containedItem.Condition / containedItem.MaxCondition) : itemContainer.Inventory.slots.Count(i => !i.Empty()) / (float)itemContainer.Inventory.capacity; if (containedItem != null && itemContainer.Inventory.Capacity == 1) { - int maxStackSize = Math.Min(containedItem.Prefab.MaxStackSize, itemContainer.MaxStackSize); + int maxStackSize = Math.Min(containedItem.Prefab.MaxStackSize, itemContainer.GetMaxStackSize(0)); if (maxStackSize > 1) { containedState = itemContainer.Inventory.slots[0].ItemCount / (float)maxStackSize; @@ -1631,7 +1632,7 @@ namespace Barotrauma int maxStackSize = item.Prefab.MaxStackSize; if (item.Container != null) { - maxStackSize = Math.Min(maxStackSize, item.Container.GetComponent()?.MaxStackSize ?? maxStackSize); + maxStackSize = Math.Min(maxStackSize, item.Container.GetComponent()?.GetMaxStackSize(slotIndex) ?? maxStackSize); } if (maxStackSize > 1 && inventory != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index 237c062d3..1ed06add7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -1194,7 +1194,14 @@ namespace Barotrauma } } - if (Character.Controlled != null && Character.Controlled.SelectedConstruction != this) { return; } + if (Character.Controlled != null && Character.Controlled.SelectedConstruction != this) + { + if (Character.Controlled.SelectedConstruction?.GetComponent()?.TargetItem != this && + !Character.Controlled.HeldItems.Any(it => it.GetComponent()?.TargetItem == this)) + { + return; + } + } bool needsLayoutUpdate = false; foreach (ItemComponent ic in activeHUDs) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs index 9fad6fc1c..a6244b448 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs @@ -423,20 +423,24 @@ namespace Barotrauma //select wire if both items it's connected to are selected var selectedItems = SelectedList.Where(e => e is Item).Cast().ToList(); - foreach (Item item in selectedItems) + foreach (Item item in Item.ItemList) { - if (item.Connections == null) continue; - foreach (Connection c in item.Connections) - { - foreach (Wire w in c.Wires) - { - if (w == null || SelectedList.Contains(w.Item)) continue; + var wire = item.GetComponent(); + if (wire == null) { continue; } + Item item0 = wire.Connections[0]?.Item; + Item item1 = wire.Connections[1]?.Item; - if (w.OtherConnection(c) != null && SelectedList.Contains(w.OtherConnection(c).Item)) - { - SelectedList.Add(w.Item); - } - } + if (item0 == null && item1 != null) + { + item0 = Item.ItemList.Find(it => it.GetComponent()?.DisconnectedWires.Contains(wire) ?? false); + } + else if (item0 != null && item1 == null) + { + item1 = Item.ItemList.Find(it => it.GetComponent()?.DisconnectedWires.Contains(wire) ?? false); + } + if (item0 != null && item1 != null && SelectedList.Contains(item0) && SelectedList.Contains(item1)) + { + SelectedList.Add(item); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs index 04701beb5..9d19ccf5a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs @@ -326,7 +326,7 @@ namespace Barotrauma depthSortedDamageable.Insert(i, structure); } } - + foreach (Structure s in depthSortedDamageable) { s.DrawDamage(spriteBatch, damageEffect, editing); @@ -418,8 +418,8 @@ namespace Barotrauma float scale = 0.9f; GUIFrame hullContainer = new GUIFrame(new RectTransform( - (parentAspectRatio > aspectRatio ? new Vector2(aspectRatio / parentAspectRatio, 1.0f) : new Vector2(1.0f, parentAspectRatio / aspectRatio)) * scale, - parent.RectTransform, Anchor.Center), + (parentAspectRatio > aspectRatio ? new Vector2(aspectRatio / parentAspectRatio, 1.0f) : new Vector2(1.0f, parentAspectRatio / aspectRatio)) * scale, + parent.RectTransform, Anchor.Center), style: null) { UserData = "hullcontainer" @@ -444,9 +444,9 @@ namespace Barotrauma { if (!combinedHulls.ContainsKey(hull)) { - combinedHulls.Add(hull, new HashSet()); + combinedHulls.Add(hull, new HashSet()); } - + combinedHulls[hull].Add(linkedHull); } } @@ -454,14 +454,14 @@ namespace Barotrauma foreach (Hull hull in hullList) { Vector2 relativeHullPos = new Vector2( - (hull.WorldRect.X - worldBorders.X) / (float)worldBorders.Width, + (hull.WorldRect.X - worldBorders.X) / (float)worldBorders.Width, (worldBorders.Y - hull.WorldRect.Y) / (float)worldBorders.Height); Vector2 relativeHullSize = new Vector2(hull.Rect.Width / (float)worldBorders.Width, hull.Rect.Height / (float)worldBorders.Height); bool hideHull = combinedHulls.ContainsKey(hull) || combinedHulls.Values.Any(hh => hh.Contains(hull)); if (hideHull) { continue; } - + Color color = Color.DarkCyan * 0.8f; var hullFrame = new GUIFrame(new RectTransform(relativeHullSize, hullContainer.RectTransform) { RelativeOffset = relativeHullPos }, style: "MiniMapRoom", color: color) @@ -477,7 +477,7 @@ namespace Barotrauma MiniMapHullData data = ConstructLinkedHulls(mainHull, linkedHulls, hullContainer, worldBorders); Vector2 relativeHullPos = new Vector2( - (data.Bounds.X - worldBorders.X) / worldBorders.Width, + (data.Bounds.X - worldBorders.X) / worldBorders.Width, (worldBorders.Y - data.Bounds.Y) / worldBorders.Height); Vector2 relativeHullSize = new Vector2(data.Bounds.Width / worldBorders.Width, data.Bounds.Height / worldBorders.Height); @@ -507,9 +507,6 @@ namespace Barotrauma var (parentW, parentH) = hullContainer.Rect.Size.ToVector2(); Vector2 size = new Vector2(rect.Width / parentW, rect.Height / parentH); // TODO this won't be required if we some day switch RectTransform to use RectangleF - const float padding = 0.001f; - size.X += padding; - size.Y += padding; Vector2 pos = new Vector2(rect.X / parentW, rect.Y / parentH); GUIFrame hullFrame = new GUIFrame(new RectTransform(size, hullContainer.RectTransform) { RelativeOffset = pos }, style: "ScanLinesSeamless", color: color) @@ -586,7 +583,7 @@ namespace Barotrauma wRect.Y = -wRect.Y; var (posX, posY) = new Vector2( - (wRect.X - worldBorders.X) / (float)worldBorders.Width, + (wRect.X - worldBorders.X) / (float)worldBorders.Width, (worldBorders.Y - wRect.Y) / (float)worldBorders.Height); var (scaleX, scaleY) = new Vector2(wRect.Width / (float)worldBorders.Width, wRect.Height / (float)worldBorders.Height); @@ -629,7 +626,7 @@ namespace Barotrauma } } - if (Info.Type != SubmarineType.OutpostModule || + if (Info.Type != SubmarineType.OutpostModule || (Info.OutpostModuleInfo?.ModuleFlags.Any(f => !f.Equals("hallwayvertical", StringComparison.OrdinalIgnoreCase) && !f.Equals("hallwayhorizontal", StringComparison.OrdinalIgnoreCase)) ?? true)) { if (!WayPoint.WayPointList.Any(wp => wp.ShouldBeSaved && wp.SpawnType == SpawnType.Path)) @@ -697,7 +694,7 @@ namespace Barotrauma int wireCount = item.Connections[i].Wires.Count(w => w != null); if (doorLinks + wireCount > item.Connections[i].MaxWires) { - errorMsgs.Add(TextManager.GetWithVariables("InsufficientFreeConnectionsWarning", + errorMsgs.Add(TextManager.GetWithVariables("InsufficientFreeConnectionsWarning", new string[] { "[doorcount]", "[freeconnectioncount]" }, new string[] { doorLinks.ToString(), (item.Connections[i].MaxWires - wireCount).ToString() })); break; @@ -794,7 +791,7 @@ namespace Barotrauma return SubEditorScreen.SuppressedWarnings.Contains(type); } } - + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { if (type != ServerNetObject.ENTITY_POSITION) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs index 182e826f9..1b5c3fdc0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs @@ -184,7 +184,7 @@ namespace Barotrauma.Particles public ParticlePrefab FindPrefab(string prefabName) { - return Prefabs.Find(p => p.Identifier == prefabName); + return Prefabs.Find(p => p.Identifier.Equals(prefabName, StringComparison.OrdinalIgnoreCase)); } private void RemoveParticle(int index) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index 071349710..29699b746 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -1366,6 +1366,7 @@ namespace Barotrauma LogButtons.Visible = GameMain.Client.HasPermission(ClientPermissions.ServerLog); GameMain.Client.ShowLogButton.Visible = GameMain.Client.HasPermission(ClientPermissions.ServerLog); roundControlsHolder.Children.ForEach(c => c.IgnoreLayoutGroups = !c.Visible); + roundControlsHolder.Children.ForEach(c => c.RectTransform.RelativeSize = Vector2.One); roundControlsHolder.Recalculate(); ReadyToStartBox.Parent.Visible = !GameMain.Client.GameStarted; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs index 0deb694c4..050fd29eb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs @@ -51,11 +51,16 @@ namespace Barotrauma } // ???????? - submarine = new Submarine(SubmarineInfo.SavedSubmarines.FirstOrDefault(info => info.Name.Equals("Kastrull", StringComparison.OrdinalIgnoreCase))); + submarine = new Submarine(SubmarineInfo.SavedSubmarines.FirstOrDefault(info => info.Name.Equals("Crescent", StringComparison.OrdinalIgnoreCase))); miniMapItem = new Item(ItemPrefab.Find(null, "statusmonitor"), Vector2.Zero, submarine); MiniMap miniMap = miniMapItem.GetComponent(); miniMap.PowerConsumption = 0; + foreach (var hull in Hull.hullList) + { + hull.WaterVolume = hull.Volume / 2f; + } + dummyCharacter = Character.Create(CharacterPrefab.HumanSpeciesName, Vector2.Zero, "", id: Entity.DummyID, hasAi: false); dummyCharacter.Info.Name = "Galldren"; dummyCharacter.Inventory.CreateSlots(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs index f6e679850..fbd068958 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs @@ -1012,7 +1012,8 @@ namespace Barotrauma Screen.Selected == GameMain.SpriteEditorScreen || Screen.Selected == GameMain.SubEditorScreen || Screen.Selected == GameMain.EventEditorScreen || - (Screen.Selected == GameMain.GameScreen && GameMain.GameSession?.GameMode is TestGameMode)) + (Screen.Selected == GameMain.GameScreen && GameMain.GameSession?.GameMode is TestGameMode) || + Screen.Selected == GameMain.NetLobbyScreen) { return "editor"; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/SpreadsheetExport.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpreadsheetExport.cs index 403a70475..6327b9e26 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/SpreadsheetExport.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/SpreadsheetExport.cs @@ -110,14 +110,14 @@ namespace Barotrauma foreach (StatusEffect statusEffect in successEffects) { float duration = statusEffect.Duration; - onSuccessAfflictions.AddRange(statusEffect.ReduceAffliction.Select(pair => Tuple.Create(GetAfflictionName(pair.First), -pair.Second, duration))); + onSuccessAfflictions.AddRange(statusEffect.ReduceAffliction.Select(pair => Tuple.Create(GetAfflictionName(pair.affliction), -pair.amount, duration))); onSuccessAfflictions.AddRange(statusEffect.Afflictions.Select(affliction => Tuple.Create(affliction.Prefab.Name, affliction.NonClampedStrength, duration))); } foreach (StatusEffect statusEffect in failureEffects) { float duration = statusEffect.Duration; - onFailureAfflictions.AddRange(statusEffect.ReduceAffliction.Select(pair => Tuple.Create(GetAfflictionName(pair.First), -pair.Second, duration))); + onFailureAfflictions.AddRange(statusEffect.ReduceAffliction.Select(pair => Tuple.Create(GetAfflictionName(pair.affliction), -pair.amount, duration))); onFailureAfflictions.AddRange(statusEffect.Afflictions.Select(affliction => Tuple.Create(affliction.Prefab.Name, affliction.NonClampedStrength, duration))); } } diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index dac5130ed..1c1a72de1 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.1500.0.0 + 0.1500.1.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 6d8757d5b..34e9c17cd 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.1500.0.0 + 0.1500.1.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index bc67e5f17..72f5373d5 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.1500.0.0 + 0.1500.1.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index d563f8dfa..98e625016 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.1500.0.0 + 0.1500.1.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 79640545b..bcaa8358d 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.1500.0.0 + 0.1500.1.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs index f9f51b713..c0998cfbc 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs @@ -28,6 +28,15 @@ namespace Barotrauma } } + if (HasAbilityFlag(AbilityFlags.RetainExperienceForNewCharacter)) + { + var ownerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == this); + if (ownerClient != null) + { + (GameMain.GameSession?.GameMode as MultiPlayerCampaign)?.SaveExperiencePoints(ownerClient); + } + } + healthUpdateTimer = 0.0f; if (CauseOfDeath.Killer != null && CauseOfDeath.Killer.IsTraitor && CauseOfDeath.Killer != this) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs index 148bfeda3..e5863ea30 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs @@ -73,6 +73,7 @@ namespace Barotrauma msg.Write(savedStatValue.RemoveOnDeath); } } + msg.Write((ushort)ExperiencePoints); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index 3996fdbeb..fef8b769a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -27,6 +27,29 @@ namespace Barotrauma public bool GameOver { get; private set; } + class SavedExperiencePoints + { + public readonly ulong SteamID; + public readonly string EndPoint; + public readonly int ExperiencePoints; + + public SavedExperiencePoints(Client client) + { + SteamID = client.SteamID; + EndPoint = client.Connection.EndPointString; + ExperiencePoints = client.Character?.Info?.ExperiencePoints ?? 0; + } + + public SavedExperiencePoints(XElement element) + { + SteamID = element.GetAttributeUInt64("steamid", 0); + EndPoint = element.GetAttributeString("endpoint", string.Empty); + ExperiencePoints = element.GetAttributeInt("points", 0); + } + } + + private readonly List savedExperiencePoints = new List(); + public override bool Paused { get { return ForceMapUI || CoroutineManager.IsCoroutineRunning("LevelTransition"); } @@ -155,6 +178,20 @@ namespace Barotrauma c.InGame && (IsOwner(c) || c.HasPermission(ClientPermissions.ManageCampaign))); } + public void SaveExperiencePoints(Client client) + { + ClearSavedExperiencePoints(client); + savedExperiencePoints.Add(new SavedExperiencePoints(client)); + } + public int GetSavedExperiencePoints(Client client) + { + return savedExperiencePoints.Find(s => s.SteamID != 0 && client.SteamID == s.SteamID || client.EndpointMatches(s.EndPoint))?.ExperiencePoints ?? 0; + } + public void ClearSavedExperiencePoints(Client client) + { + savedExperiencePoints.RemoveAll(s => s.SteamID != 0 && client.SteamID == s.SteamID || client.EndpointMatches(s.EndPoint)); + } + public void LoadPets() { if (petsElement != null) @@ -259,6 +296,16 @@ namespace Barotrauma Map.ProgressWorld(transitionType, (float)(Timing.TotalTime - GameMain.GameSession.RoundStartTime)); bool success = GameMain.Server.ConnectedClients.Any(c => c.InGame && c.Character != null && !c.Character.IsDead); + if (success) + { + foreach (Client c in GameMain.Server.ConnectedClients) + { + if (c.Character?.HasAbilityFlag(AbilityFlags.RetainExperienceForNewCharacter) ?? false) + { + (GameMain.GameSession?.GameMode as MultiPlayerCampaign)?.SaveExperiencePoints(c); + } + } + } GameMain.GameSession.EndRound("", traitorResults, transitionType); @@ -965,6 +1012,15 @@ namespace Barotrauma // save bots CrewManager.SaveMultiplayer(modeElement); + XElement savedExperiencePointsElement = new XElement("SavedExperiencePoints"); + foreach (var savedExperiencePoint in savedExperiencePoints) + { + savedExperiencePointsElement.Add(new XElement("Point", + new XAttribute("steamid", savedExperiencePoint.SteamID), + new XAttribute("endpoint", savedExperiencePoint?.EndPoint ?? string.Empty), + new XAttribute("points", savedExperiencePoint.ExperiencePoints))); + } + // save available submarines XElement availableSubsElement = new XElement("AvailableSubs"); for (int i = 0; i < GameMain.NetLobbyScreen.CampaignSubmarines.Count; i++) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/GeneticMaterial.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/GeneticMaterial.cs new file mode 100644 index 000000000..460c5ec46 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/GeneticMaterial.cs @@ -0,0 +1,20 @@ +using Barotrauma.Networking; + +namespace Barotrauma.Items.Components +{ + partial class GeneticMaterial : ItemComponent + { + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + { + msg.Write(tainted); + if (tainted) + { + msg.Write(selectedTaintedEffect?.UIntIdentifier ?? 0); + } + else + { + msg.Write(selectedEffect?.UIntIdentifier ?? 0); + } + } + } +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs index 1382bdc27..517f51201 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs @@ -19,13 +19,13 @@ namespace Barotrauma.Items.Components GameServer.Log(GameServer.CharacterLogName(c.Character) + " entered \"" + newOutputValue + "\" on " + item.Name, ServerLog.MessageType.ItemInteraction); OutputValue = newOutputValue; - ShowOnDisplay(newOutputValue); + ShowOnDisplay(newOutputValue, addToHistory: true); item.SendSignal(newOutputValue, "signal_out"); item.CreateServerEvent(this); } } - partial void ShowOnDisplay(string input, bool addToHistory = true) + partial void ShowOnDisplay(string input, bool addToHistory) { if (addToHistory) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 2df0be84f..575f5c157 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -2358,6 +2358,12 @@ namespace Barotrauma.Networking characterData.HasSpawned = true; } + if (GameMain.GameSession?.GameMode is MultiPlayerCampaign mpCampaign && spawnedCharacter.Info != null) + { + spawnedCharacter.Info.SetExperience(Math.Max(spawnedCharacter.Info.ExperiencePoints, mpCampaign.GetSavedExperiencePoints(teamClients[i]))); + mpCampaign.ClearSavedExperiencePoints(teamClients[i]); + } + spawnedCharacter.OwnerClientEndPoint = teamClients[i].Connection.EndPointString; spawnedCharacter.OwnerClientName = teamClients[i].Name; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index d162db6f0..5430e285c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -379,6 +379,12 @@ namespace Barotrauma.Networking } else { + if (GameMain.GameSession?.GameMode is MultiPlayerCampaign mpCampaign && character.Info != null) + { + character.Info.SetExperience(Math.Max(character.Info.ExperiencePoints, mpCampaign.GetSavedExperiencePoints(clients[i]))); + mpCampaign.ClearSavedExperiencePoints(clients[i]); + } + //tell the respawning client they're no longer a traitor if (GameMain.Server.TraitorManager?.Traitors != null && clients[i].Character != null) { diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 879a52022..e4aa06d3c 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.1500.0.0 + 0.1500.1.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 f5c2f7d19..a7a1728a0 100644 --- a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml +++ b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml @@ -23,6 +23,7 @@ + @@ -63,6 +64,7 @@ + @@ -152,6 +154,7 @@ + diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 6a86228e6..a448481c0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -1915,12 +1915,12 @@ namespace Barotrauma #if SERVER GameMain.NetworkMember.CreateEntityEvent(Character, new object[] { - Networking.NetEntityEvent.Type.SetAttackTarget, - attackingLimb, - (damageTarget as Entity)?.ID ?? Entity.NullEntityID, - damageTarget is Character character && targetLimb != null ? Array.IndexOf(character.AnimController.Limbs, targetLimb) : 0, - SimPosition.X, - SimPosition.Y + Networking.NetEntityEvent.Type.SetAttackTarget, + attackingLimb, + (damageTarget as Entity)?.ID ?? Entity.NullEntityID, + damageTarget is Character character && targetLimb != null ? Array.IndexOf(character.AnimController.Limbs, targetLimb) : 0, + SimPosition.X, + SimPosition.Y }); #else Character.PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index 79842539a..e554fb317 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -588,8 +588,9 @@ namespace Barotrauma // assume that it's required for the stun effect // as we can't check the status effect conditions here. var mobileBatteryTag = "mobilebattery"; - var containers = weapon.Item.Components.Where(ic => ic is ItemContainer container && - container.ContainableItems.Any(containable => containable.Identifiers.Any(id => id.Equals(mobileBatteryTag)))); + var containers = weapon.Item.Components.Where(ic => + ic is ItemContainer container && + container.ContainableItemIdentifiers.Contains(mobileBatteryTag)); // If there's no such container, assume that the melee weapon can stun without a battery. return containers.None() || containers.Any(container => (container as ItemContainer)?.Inventory.AllItems.Any(i => i != null && i.HasTag(mobileBatteryTag) && i.Condition > 0.0f) ?? false); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs index 702670425..8d490a3fa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs @@ -148,7 +148,7 @@ namespace Barotrauma public virtual void UpdateAnim(float deltaTime) { } - public virtual void HoldItem(float deltaTime, Item item, Vector2[] handlePos, Vector2 holdPos, Vector2 aimPos, bool aim, float holdAngle, float itemAngleRelativeToHoldAngle = 0.0f) { } + public virtual void HoldItem(float deltaTime, Item item, Vector2[] handlePos, Vector2 holdPos, Vector2 aimPos, bool aim, float holdAngle, float itemAngleRelativeToHoldAngle = 0.0f, bool aimingMelee = false) { } public virtual void DragCharacter(Character target, float deltaTime) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs index 475a16a08..a057b932d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs @@ -390,11 +390,17 @@ namespace Barotrauma } if (eatTimer % 1.0f < 0.5f && (eatTimer - deltaTime * eatSpeed) % 1.0f > 0.5f) { - bool CanBeSevered(LimbJoint j) => !j.IsSevered && j.CanBeSevered && j.LimbA != null && !j.LimbA.IsSevered && j.LimbB != null && !j.LimbB.IsSevered; + static bool CanBeSevered(LimbJoint j) => !j.IsSevered && j.CanBeSevered && j.LimbA != null && !j.LimbA.IsSevered && j.LimbB != null && !j.LimbB.IsSevered; //keep severing joints until there is only one limb left var nonSeveredJoints = target.AnimController.LimbJoints.Where(CanBeSevered); if (nonSeveredJoints.None()) { + //small monsters don't eat the contents of the character's inventory + if (Mass < target.AnimController.Mass) + { + target.Inventory?.AllItemsMod.ForEach(it => it?.Drop(dropper: null)); + } + //only one limb left, the character is now full eaten Entity.Spawner?.AddToRemoveQueue(target); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index 1e910d566..5032d2a2e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -150,6 +150,13 @@ namespace Barotrauma private float upperLegLength = 0.0f, lowerLegLength = 0.0f; private bool aiming; + private bool wasAiming; + + private bool aimingMelee; + private bool wasAimingMelee; + + public bool IsAiming => wasAiming; + public bool IsAimingMelee => wasAimingMelee; private readonly float movementLerp; @@ -532,7 +539,10 @@ namespace Barotrauma limb.Disabled = false; } + wasAiming = aiming; aiming = false; + wasAimingMelee = aimingMelee; + aimingMelee = false; if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) return; } @@ -1718,7 +1728,7 @@ namespace Barotrauma } //TODO: refactor this method, it's way too convoluted - public override void HoldItem(float deltaTime, Item item, Vector2[] handlePos, Vector2 holdPos, Vector2 aimPos, bool aim, float holdAngle, float itemAngleRelativeToHoldAngle = 0.0f) + public override void HoldItem(float deltaTime, Item item, Vector2[] handlePos, Vector2 holdPos, Vector2 aimPos, bool aim, float holdAngle, float itemAngleRelativeToHoldAngle = 0.0f, bool aimingMelee = false) { if (character.Stun > 0.0f || character.IsIncapacitated) { @@ -1748,6 +1758,8 @@ namespace Barotrauma Holdable holdable = item.GetComponent(); + this.aimingMelee = aimingMelee; + if (!isClimbing && !usingController && character.Stun <= 0.0f && aim && itemPos != Vector2.Zero && !character.IsIncapacitated) { Vector2 mousePos = ConvertUnits.ToSimUnits(character.SmoothedCursorPosition); @@ -1771,7 +1783,6 @@ namespace Barotrauma aiming = true; } - } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index 43d9dc327..3bbb32214 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -395,12 +395,18 @@ namespace Barotrauma if (character.IsHusk && character.Params.UseHuskAppendage) { + bool inEditor = false; +#if CLIENT + inEditor = Screen.Selected == GameMain.CharacterEditorScreen; +#endif + var characterPrefab = CharacterPrefab.FindByFilePath(character.ConfigPath); if (characterPrefab?.XDocument != null) { var mainElement = characterPrefab.XDocument.Root.IsOverride() ? characterPrefab.XDocument.Root.FirstElement() : characterPrefab.XDocument.Root; foreach (var huskAppendage in mainElement.GetChildElements("huskappendage")) { + if (!inEditor && huskAppendage.GetAttributeBool("onlyfromafflictions", false)) { continue; } AfflictionHusk.AttachHuskAppendage(character, huskAppendage.GetAttributeString("affliction", string.Empty), huskAppendage, ragdoll: this); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index dc2f44f07..e07f73f0c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -8,7 +8,8 @@ namespace Barotrauma public enum HitDetection { Distance, - Contact + Contact, + None } public enum AttackContext @@ -74,20 +75,6 @@ namespace Barotrauma } } - class AttackData - { - public float DamageMultiplier { get; set; } = 1f; - public float AddedPenetration { get; set; } = 0f; - public List Afflictions { get; set; } - public Attack SourceAttack { get; } - - public AttackData(Attack sourceAttack) - { - SourceAttack = sourceAttack; - } - - } - partial class Attack : ISerializableEntity { [Serialize(AttackContext.Any, true, description: "The attack will be used only in this context."), Editable] @@ -466,7 +453,7 @@ namespace Barotrauma DamageParticles(deltaTime, worldPosition); - var attackResult = target.AddDamage(attacker, worldPosition, this, deltaTime, playSound); + var attackResult = target?.AddDamage(attacker, worldPosition, this, deltaTime, playSound) ?? new AttackResult(); var effectType = attackResult.Damage > 0.0f ? ActionType.OnUse : ActionType.OnFailure; if (targetCharacter != null && targetCharacter.IsDead) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index c97e76569..f442f7cc1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -9,6 +9,7 @@ using System.Xml.Linq; using Barotrauma.Items.Components; using FarseerPhysics.Dynamics; using Barotrauma.Extensions; +using Barotrauma.Abilities; #if SERVER using System.Text; #endif @@ -1366,7 +1367,6 @@ namespace Barotrauma } } } - private List wearableItems = new List(); public float GetSkillLevel(string skillIdentifier) { @@ -2075,11 +2075,10 @@ namespace Barotrauma return SelectedCharacter == owner && owner.CanInventoryBeAccessed; } - if (inventory.Owner is Item) + if (inventory.Owner is Item item) { - var owner = (Item)inventory.Owner; - if (!CanInteractWith(owner) && !owner.linkedTo.Any(lt => lt is Item item && item.DisplaySideBySideWhenLinked && CanInteractWith(item))) { return false; } - ItemContainer container = owner.GetComponents().FirstOrDefault(ic => ic.Inventory == inventory); + if (!CanInteractWith(item) && !item.linkedTo.Any(lt => lt is Item item && item.DisplaySideBySideWhenLinked && CanInteractWith(item))) { return false; } + ItemContainer container = item.GetComponents().FirstOrDefault(ic => ic.Inventory == inventory); if (container != null && !container.HasRequiredItems(this, addMessage: false)) { return false; } } return true; @@ -2218,6 +2217,12 @@ namespace Barotrauma } } + if (SelectedConstruction?.GetComponent()?.TargetItem == item || + HeldItems.Any(it => it.GetComponent()?.TargetItem == item)) + { + return true; + } + if (item.InteractDistance == 0.0f && !item.Prefab.Triggers.Any()) { return false; } Pickable pickableComponent = item.GetComponent(); @@ -3355,10 +3360,18 @@ namespace Barotrauma float attackImpulse = attack.TargetImpulse + attack.TargetForce * deltaTime; - AttackData attackData = new AttackData(attack); - attacker.CheckTalents(AbilityEffectType.OnAttack, attackData); - CheckTalents(AbilityEffectType.OnAttacked, attackData); - attackData.DamageMultiplier *= (1 + attacker.GetStatValue(StatTypes.AttackMultiplier)); + AbilityAttackData attackData = new AbilityAttackData(attack, this); + if (attacker != null) + { + attackData.Attacker = attacker; + attacker.CheckTalents(AbilityEffectType.OnAttack, attackData); + CheckTalents(AbilityEffectType.OnAttacked, attackData); + attackData.DamageMultiplier *= 1 + attacker.GetStatValue(StatTypes.AttackMultiplier); + if (attacker.TeamID == TeamID) + { + attackData.DamageMultiplier *= 1 + attacker.GetStatValue(StatTypes.TeamAttackMultiplier); + } + } IEnumerable attackAfflictions; @@ -3495,6 +3508,7 @@ namespace Barotrauma { attackerCrewmember.CheckTalents(AbilityEffectType.OnCrewKillCharacter, target); } + CheckTalents(AbilityEffectType.OnKillCharacter, target); if (!IsOnPlayerTeam) { return; } if (GameMain.Config.KilledCreatures.Any(name => name.Equals(target.SpeciesName, StringComparison.OrdinalIgnoreCase))) { return; } @@ -3653,10 +3667,7 @@ namespace Barotrauma if (statusEffect.type != actionType) { continue; } if (statusEffect.type == ActionType.OnDamaged) { - if (statusEffect.AllowedAfflictions != null && (LastDamage.Afflictions == null || LastDamage.Afflictions.None(a => statusEffect.AllowedAfflictions.Contains(a.Prefab.AfflictionType) || statusEffect.AllowedAfflictions.Contains(a.Prefab.Identifier)))) - { - continue; - } + if (!statusEffect.HasRequiredAfflictions(LastDamage)) { continue; } if (statusEffect.OnlyPlayerTriggered) { if (LastAttacker == null || !LastAttacker.IsPlayer) @@ -3719,7 +3730,7 @@ namespace Barotrauma } } - private void Implode(bool isNetworkMessage = false) + public void Implode(bool isNetworkMessage = false) { if (CharacterHealth.Unkillable || GodMode || IsDead) { return; } @@ -3829,7 +3840,7 @@ namespace Barotrauma if (info != null) { info.CauseOfDeath = CauseOfDeath; - info.ResetSavedStatValues(); + info.MissionsCompletedSinceDeath = 0; } AnimController.movement = Vector2.Zero; AnimController.TargetMovement = Vector2.Zero; @@ -4434,7 +4445,7 @@ namespace Barotrauma } } - private StatTypes GetSkillStatType(string skillIdentifier) + public static StatTypes GetSkillStatType(string skillIdentifier) { // Using this method to translate between skill identifiers and stat types. Feel free to replace it if there's a better way switch (skillIdentifier) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index 9526ce3a4..b4b617328 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -462,6 +462,9 @@ namespace Barotrauma public bool IsAttachmentsLoaded => HairIndex > -1 && BeardIndex > -1 && MoustacheIndex > -1 && FaceAttachmentIndex > -1; + // talent-relevant values + public int MissionsCompletedSinceDeath = 0; + // Used for creating the data public CharacterInfo(string speciesName, string name = "", string originalName = "", JobPrefab jobPrefab = null, string ragdollFileName = null, int variant = 0, Rand.RandSync randSync = Rand.RandSync.Unsynced, string npcIdentifier = "") { @@ -605,7 +608,10 @@ namespace Barotrauma if (!string.IsNullOrEmpty(personalityName)) { personalityTrait = NPCPersonalityTrait.List.Find(p => p.Name == personalityName); - } + } + + MissionsCompletedSinceDeath = infoElement.GetAttributeInt("missionscompletedsincedeath", 0); + foreach (XElement subElement in infoElement.Elements()) { bool jobCreated = false; @@ -973,16 +979,17 @@ namespace Barotrauma } float prevLevel = Job.GetSkillLevel(skillIdentifier); - Job.IncreaseSkillLevel(skillIdentifier, increase); + Job.IncreaseSkillLevel(skillIdentifier, increase, Character.HasAbilityFlag(AbilityFlags.GainSkillPastMaximum)); float newLevel = Job.GetSkillLevel(skillIdentifier); + if ((int)newLevel > (int)prevLevel) { Character.CheckTalents(AbilityEffectType.OnGainSkillPoint, skillIdentifier); - foreach (Character character in Character.GetFriendlyCrew(Character)) { - character.CheckTalents(AbilityEffectType.OnAllyGainSkillPoint, (skillIdentifier, Character)); + var abilityStringCharacter = new AbilityStringCharacter(skillIdentifier, Character); + character.CheckTalents(AbilityEffectType.OnAllyGainSkillPoint, abilityStringCharacter); } } @@ -1139,9 +1146,10 @@ namespace Barotrauma new XAttribute("startitemsgiven", StartItemsGiven), new XAttribute("ragdoll", ragdollFileName), new XAttribute("personality", personalityTrait == null ? "" : personalityTrait.Name)); - // TODO: animations? + charElement.Add(new XAttribute("missionscompletedsincedeath", MissionsCompletedSinceDeath)); + if (Character != null) { if (Character.AnimController.CurrentHull != null) @@ -1158,6 +1166,7 @@ namespace Barotrauma foreach (var savedStat in statValuePair.Value) { if (savedStat.StatValue == 0f) { continue; } + if (savedStat.RemoveAfterRound) { continue; } savedStatElement.Add(new XElement("savedstatvalue", new XAttribute("stattype", statValuePair.Key.ToString()), @@ -1168,6 +1177,8 @@ namespace Barotrauma } } + + charElement.Add(savedStatElement); parentElement.Add(charElement); @@ -1496,7 +1507,6 @@ namespace Barotrauma } } } - public void ResetSavedStatValue(string statIdentifier) { savedStatValues.SelectMany(s => s.Value).Where(s => s.StatIdentifier == statIdentifier).ForEach(v => v.StatValue = 0f); @@ -1514,7 +1524,7 @@ namespace Barotrauma } } - public void ChangeSavedStatValue(StatTypes statType, float value, string statIdentifier, bool removeOnDeath) + public void ChangeSavedStatValue(StatTypes statType, float value, string statIdentifier, bool removeOnDeath, bool removeAfterRound = false, float maxValue = float.MaxValue) { if (!savedStatValues.ContainsKey(statType)) { @@ -1523,12 +1533,11 @@ namespace Barotrauma if (savedStatValues[statType].FirstOrDefault(s => s.StatIdentifier == statIdentifier) is SavedStatValue savedStat) { - savedStat.StatValue += value; - savedStat.RemoveOnDeath = removeOnDeath; + savedStat.StatValue = MathHelper.Min(savedStat.StatValue + value, maxValue); } else { - savedStatValues[statType].Add(new SavedStatValue(statIdentifier, value, removeOnDeath)); + savedStatValues[statType].Add(new SavedStatValue(statIdentifier, MathHelper.Min(value, maxValue), removeOnDeath, removeAfterRound)); } } } @@ -1538,12 +1547,14 @@ namespace Barotrauma public string StatIdentifier { get; set; } public float StatValue { get; set; } public bool RemoveOnDeath { get; set; } + public bool RemoveAfterRound { get; set; } - public SavedStatValue(string statIdentifier, float value, bool removeOnDeath) + public SavedStatValue(string statIdentifier, float value, bool removeOnDeath, bool retainAfterRound) { StatValue = value; RemoveOnDeath = removeOnDeath; StatIdentifier = statIdentifier; + RemoveAfterRound = retainAfterRound; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs index 96b550b47..d67a18fde 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs @@ -17,6 +17,8 @@ namespace Barotrauma public float PendingAdditionStrength { get; set; } public float AdditionStrength { get; set; } + private float fluctuationTimer; + protected float _strength; [Serialize(0f, true), Editable] @@ -56,6 +58,8 @@ namespace Barotrauma public readonly Dictionary PeriodicEffectTimers = new Dictionary(); + public double AppliedAsSuccessfulTreatmentTime, AppliedAsFailedTreatmentTime; + /// /// Which character gave this affliction /// @@ -123,7 +127,7 @@ namespace Barotrauma float amount = MathHelper.Lerp( currentEffect.MinGrainStrength, currentEffect.MaxGrainStrength, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); + (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)) * GetScreenEffectFluctuation(currentEffect); if (Prefab.GrainBurst > 0 && AdditionStrength > amount) { @@ -138,12 +142,12 @@ namespace Barotrauma if (Strength < Prefab.ActivationThreshold) { return 0.0f; } AfflictionPrefab.Effect currentEffect = GetActiveEffect(); if (currentEffect == null) { return 0.0f; } - if (currentEffect.MaxScreenDistortStrength - currentEffect.MinScreenDistortStrength < 0.0f) { return 0.0f; } + if (currentEffect.MaxScreenDistort - currentEffect.MinScreenDistort < 0.0f) { return 0.0f; } return MathHelper.Lerp( - currentEffect.MinScreenDistortStrength, - currentEffect.MaxScreenDistortStrength, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); + currentEffect.MinScreenDistort, + currentEffect.MaxScreenDistort, + (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)) * GetScreenEffectFluctuation(currentEffect); } public float GetRadialDistortStrength() @@ -151,12 +155,12 @@ namespace Barotrauma if (Strength < Prefab.ActivationThreshold) { return 0.0f; } AfflictionPrefab.Effect currentEffect = GetActiveEffect(); if (currentEffect == null) { return 0.0f; } - if (currentEffect.MaxRadialDistortStrength - currentEffect.MinRadialDistortStrength < 0.0f) { return 0.0f; } + if (currentEffect.MaxRadialDistort - currentEffect.MinRadialDistort < 0.0f) { return 0.0f; } return MathHelper.Lerp( - currentEffect.MinRadialDistortStrength, - currentEffect.MaxRadialDistortStrength, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); + currentEffect.MinRadialDistort, + currentEffect.MaxRadialDistort, + (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)) * GetScreenEffectFluctuation(currentEffect); } public float GetChromaticAberrationStrength() @@ -164,12 +168,12 @@ namespace Barotrauma if (Strength < Prefab.ActivationThreshold) { return 0.0f; } AfflictionPrefab.Effect currentEffect = GetActiveEffect(); if (currentEffect == null) { return 0.0f; } - if (currentEffect.MaxChromaticAberrationStrength - currentEffect.MinChromaticAberrationStrength < 0.0f) { return 0.0f; } + if (currentEffect.MaxChromaticAberration - currentEffect.MinChromaticAberration < 0.0f) { return 0.0f; } return MathHelper.Lerp( - currentEffect.MinChromaticAberrationStrength, - currentEffect.MaxChromaticAberrationStrength, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); + currentEffect.MinChromaticAberration, + currentEffect.MaxChromaticAberration, + (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)) * GetScreenEffectFluctuation(currentEffect); } public float GetScreenBlurStrength() @@ -177,12 +181,18 @@ namespace Barotrauma if (Strength < Prefab.ActivationThreshold) { return 0.0f; } AfflictionPrefab.Effect currentEffect = GetActiveEffect(); if (currentEffect == null) { return 0.0f; } - if (currentEffect.MaxScreenBlurStrength - currentEffect.MinScreenBlurStrength < 0.0f) { return 0.0f; } + if (currentEffect.MaxScreenBlur - currentEffect.MinScreenBlur < 0.0f) { return 0.0f; } return MathHelper.Lerp( - currentEffect.MinScreenBlurStrength, - currentEffect.MaxScreenBlurStrength, - (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); + currentEffect.MinScreenBlur, + currentEffect.MaxScreenBlur, + (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)) * GetScreenEffectFluctuation(currentEffect); + } + + private float GetScreenEffectFluctuation(AfflictionPrefab.Effect currentEffect) + { + if (currentEffect == null || currentEffect.ScreenEffectFluctuationFrequency <= 0.0f) { return 1.0f; } + return ((float)Math.Sin(fluctuationTimer * MathHelper.TwoPi) + 1.0f) * 0.5f; } public float GetSkillMultiplier() @@ -210,14 +220,17 @@ namespace Barotrauma } } - public float GetResistance(string afflictionId) + public float GetResistance(AfflictionPrefab affliction) { if (Strength < Prefab.ActivationThreshold) { return 0.0f; } AfflictionPrefab.Effect currentEffect = GetActiveEffect(); if (currentEffect == null) { return 0.0f; } - if (currentEffect.MaxResistance - currentEffect.MinResistance <= 0.0f) { return 0.0f; } - if (afflictionId != null && afflictionId != currentEffect.ResistanceFor) { return 0.0f; } - + if (!currentEffect.ResistanceFor.Any(r => + r.Equals(affliction.Identifier, StringComparison.OrdinalIgnoreCase) || + r.Equals(affliction.AfflictionType, StringComparison.OrdinalIgnoreCase))) + { + return 0.0f; + } return MathHelper.Lerp( currentEffect.MinResistance, currentEffect.MaxResistance, @@ -229,8 +242,6 @@ namespace Barotrauma if (Strength < Prefab.ActivationThreshold) { return 1.0f; } AfflictionPrefab.Effect currentEffect = GetActiveEffect(); if (currentEffect == null) { return 1.0f; } - if (currentEffect.MaxSpeedMultiplier - currentEffect.MinSpeedMultiplier <= 0.0f) { return 1.0f; } - return MathHelper.Lerp( currentEffect.MinSpeedMultiplier, currentEffect.MaxSpeedMultiplier, @@ -282,6 +293,9 @@ namespace Barotrauma AfflictionPrefab.Effect currentEffect = GetActiveEffect(); if (currentEffect == null) { return; } + fluctuationTimer += deltaTime * currentEffect.ScreenEffectFluctuationFrequency; + fluctuationTimer %= 1.0f; + if (currentEffect.StrengthChange < 0) // Reduce diminishing of buffs if boosted { float durationMultiplier = 1 / (1 + (Prefab.IsBuff ? characterHealth.Character.GetStatValue(StatTypes.BuffDurationMultiplier) @@ -290,9 +304,9 @@ namespace Barotrauma _strength += currentEffect.StrengthChange * deltaTime * StrengthDiminishMultiplier * durationMultiplier; } - else // Reduce strengthening of afflictions if resistant + else if (currentEffect.StrengthChange > 0) // Reduce strengthening of afflictions if resistant { - _strength += currentEffect.StrengthChange * deltaTime * (1f - characterHealth.GetResistance(Prefab.Identifier)); + _strength += currentEffect.StrengthChange * deltaTime * (1f - characterHealth.GetResistance(Prefab)); } // Don't use the property, because it's virtual and some afflictions like husk overload it for external use. _strength = MathHelper.Clamp(_strength, 0.0f, Prefab.MaxStrength); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs index 82f12e892..72c3dfae7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs @@ -34,6 +34,10 @@ namespace Barotrauma float threshold = _strength > ActiveThreshold ? ActiveThreshold + 1 : DormantThreshold - 1; float max = Math.Max(threshold, previousValue); _strength = Math.Clamp(value, 0, max); + if (previousValue > 0.0f && value <= 0.0f) + { + DeactivateHusk(); + } } } @@ -51,8 +55,10 @@ namespace Barotrauma } } - private float DormantThreshold => Prefab.MaxStrength * 0.5f; - private float ActiveThreshold => Prefab.MaxStrength * 0.75f; + private float DormantThreshold => (Prefab as AfflictionPrefabHusk)?.DormantThreshold ?? Prefab.MaxStrength * 0.5f; + private float ActiveThreshold => (Prefab as AfflictionPrefabHusk)?.ActiveThreshold ?? Prefab.MaxStrength * 0.75f; + + private float TransitionThreshold => (Prefab as AfflictionPrefabHusk)?.TransitionThreshold ?? Prefab.MaxStrength * 0.75f; public AfflictionHusk(AfflictionPrefab prefab, float strength) : base(prefab, strength) { } @@ -83,7 +89,7 @@ namespace Barotrauma } State = InfectionState.Transition; } - else if (Strength < Prefab.MaxStrength) + else if (Strength < TransitionThreshold) { if (State != InfectionState.Active) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index 619fb81f0..5028df129 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -97,6 +97,10 @@ namespace Barotrauma CauseSpeechImpediment = element.GetAttributeBool("causespeechimpediment", true); NeedsAir = element.GetAttributeBool("needsair", false); ControlHusk = element.GetAttributeBool("controlhusk", false); + + DormantThreshold = element.GetAttributeFloat("dormantthreshold", MaxStrength * 0.5f); + ActiveThreshold = element.GetAttributeFloat("activethreshold", MaxStrength * 0.75f); + TransitionThreshold = element.GetAttributeFloat("transitionthreshold", MaxStrength); } // Use any of these to define which limb the appendage is attached to. @@ -105,6 +109,8 @@ namespace Barotrauma public readonly string AttachLimbName; public readonly LimbType AttachLimbType; + public float ActiveThreshold, DormantThreshold, TransitionThreshold; + public readonly string HuskedSpeciesName; public readonly string[] TargetSpecies; public const string Tag = "[speciesname]"; @@ -141,28 +147,31 @@ namespace Barotrauma public bool MultiplyByMaxVitality { get; private set; } [Serialize(0.0f, false)] - public float MinScreenBlurStrength { get; private set; } + public float MinScreenBlur { get; private set; } [Serialize(0.0f, false)] - public float MaxScreenBlurStrength { get; private set; } + public float MaxScreenBlur { get; private set; } [Serialize(0.0f, false)] - public float MinScreenDistortStrength { get; private set; } + public float MinScreenDistort { get; private set; } [Serialize(0.0f, false)] - public float MaxScreenDistortStrength { get; private set; } + public float MaxScreenDistort { get; private set; } [Serialize(0.0f, false)] - public float MinRadialDistortStrength { get; private set; } + public float MinRadialDistort { get; private set; } [Serialize(0.0f, false)] - public float MaxRadialDistortStrength { get; private set; } + public float MaxRadialDistort { get; private set; } [Serialize(0.0f, false)] - public float MinChromaticAberrationStrength { get; private set; } + public float MinChromaticAberration { get; private set; } [Serialize(0.0f, false)] - public float MaxChromaticAberrationStrength { get; private set; } + public float MaxChromaticAberration { get; private set; } + + [Serialize("255,255,255,255", false)] + public Color GrainColor { get; private set; } [Serialize(0.0f, false)] public float MinGrainStrength { get; private set; } @@ -170,6 +179,9 @@ namespace Barotrauma [Serialize(0.0f, false)] public float MaxGrainStrength { get; private set; } + [Serialize(0.0f, false)] + public float ScreenEffectFluctuationFrequency { get; private set; } + [Serialize(1.0f, false)] public float MinBuffMultiplier { get; private set; } @@ -188,8 +200,11 @@ namespace Barotrauma [Serialize(1.0f, false)] public float MaxSkillMultiplier { get; private set; } - [Serialize("", false)] - public string ResistanceFor { get; private set; } + private readonly string[] resistanceFor; + public IEnumerable ResistanceFor + { + get { return resistanceFor; } + } [Serialize(0.0f, false)] public float MinResistance { get; private set; } @@ -209,6 +224,8 @@ namespace Barotrauma { SerializableProperty.DeserializeProperties(this, element); + resistanceFor = element.GetAttributeStringArray("resistancefor", new string[0], convertToLowerInvariant: true); + foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index a4908115e..a195d5620 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -266,6 +266,12 @@ namespace Barotrauma private LimbHealth GetMatchingLimbHealth(Limb limb) => limb == null ? null : limbHealths[limb.HealthIndex]; private LimbHealth GetMatchingLimbHealth(Affliction affliction) => GetMatchingLimbHealth(Character.AnimController.GetLimb(affliction.Prefab.IndicatorLimb, excludeSevered: false)); + /// + /// Returns the limb afflictions and non-limbspecific afflictions that are set to be displayed on this limb. + /// + private IEnumerable GetMatchingAfflictions(LimbHealth limb) + => limb.Afflictions.Union(afflictions.Where(a => GetMatchingLimbHealth(a) == limb)); + /// /// Returns the limb afflictions and non-limbspecific afflictions that are set to be displayed on this limb. /// @@ -426,18 +432,14 @@ namespace Barotrauma } } - public float GetResistance(string resistanceId) + public float GetResistance(AfflictionPrefab affliction) { float resistance = 0.0f; for (int i = 0; i < afflictions.Count; i++) { - if (!afflictions[i].Prefab.IsBuff) continue; - float temp = afflictions[i].GetResistance(resistanceId); - if (temp > resistance) resistance = temp; + resistance += afflictions[i].GetResistance(affliction); } - resistance = 1 - ((1 - resistance) * Character.GetAbilityResistance(resistanceId)); - - return resistance; + return 1 - ((1 - resistance) * Character.GetAbilityResistance(affliction.Identifier)); } public float GetStatValue(StatTypes statType) @@ -451,7 +453,7 @@ namespace Barotrauma } private readonly List matchingAfflictions = new List(); - public void ReduceAffliction(Limb targetLimb, string affliction, float amount) + public void ReduceAffliction(Limb targetLimb, string affliction, float amount, ActionType? treatmentAction = null) { matchingAfflictions.Clear(); matchingAfflictions.AddRange(afflictions); @@ -499,6 +501,17 @@ namespace Barotrauma { matchingAffliction.Strength -= reduceAmount; amount -= reduceAmount; + if (treatmentAction != null) + { + if (treatmentAction.Value == ActionType.OnUse) + { + matchingAffliction.AppliedAsSuccessfulTreatmentTime = Timing.TotalTime; + } + else if (treatmentAction.Value == ActionType.OnFailure) + { + matchingAffliction.AppliedAsFailedTreatmentTime = Timing.TotalTime; + } + } } } CalculateVitality(); @@ -610,7 +623,7 @@ namespace Barotrauma { if (newAffliction.Prefab == affliction.Prefab) { - float newStrength = newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(affliction.Prefab.Identifier)); + float newStrength = newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(affliction.Prefab)); if (allowStacking) { // Add the existing strength @@ -632,7 +645,7 @@ namespace Barotrauma //create a new instance of the affliction to make sure we don't use the same instance for multiple characters //or modify the affliction instance of an Attack or a StatusEffect var copyAffliction = newAffliction.Prefab.Instantiate( - Math.Min(newAffliction.Prefab.MaxStrength, newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(newAffliction.Prefab.Identifier))), + Math.Min(newAffliction.Prefab.MaxStrength, newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(newAffliction.Prefab))), newAffliction.Source); limbHealth.Afflictions.Add(copyAffliction); @@ -666,7 +679,7 @@ namespace Barotrauma { if (newAffliction.Prefab == affliction.Prefab) { - float newStrength = newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(affliction.Prefab.Identifier)); + float newStrength = newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(affliction.Prefab)); if (allowStacking) { // Add the existing strength @@ -688,7 +701,7 @@ namespace Barotrauma //create a new instance of the affliction to make sure we don't use the same instance for multiple characters //or modify the affliction instance of an Attack or a StatusEffect afflictions.Add(newAffliction.Prefab.Instantiate( - Math.Min(newAffliction.Prefab.MaxStrength, newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(newAffliction.Prefab.Identifier))), + Math.Min(newAffliction.Prefab.MaxStrength, newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(newAffliction.Prefab))), source: newAffliction.Source)); Character.HealthUpdateInterval = 0.0f; @@ -700,8 +713,6 @@ namespace Barotrauma } } - partial void UpdateProjSpecific(float deltaTime); - partial void UpdateLimbAfflictionOverlays(); public void Update(float deltaTime) @@ -741,7 +752,7 @@ namespace Barotrauma for (int i = afflictions.Count - 1; i >= 0; i--) { var affliction = afflictions[i]; - if (irremovableAfflictions.Contains(affliction)) continue; + if (irremovableAfflictions.Contains(affliction)) { continue; } if (affliction.Strength <= 0.0f) { SteamAchievementManager.OnAfflictionRemoved(affliction, Character); @@ -763,6 +774,10 @@ namespace Barotrauma { Character.StackSpeedMultiplier(1f + Character.GetStatValue(StatTypes.SwimmingSpeed)); } + else + { + Character.StackSpeedMultiplier(1f + Character.GetStatValue(StatTypes.WalkingSpeed)); + } UpdateLimbAfflictionOverlays(); @@ -786,7 +801,12 @@ namespace Barotrauma } else { - OxygenAmount = MathHelper.Clamp(OxygenAmount + deltaTime * (Character.OxygenAvailable < InsufficientOxygenThreshold ? -5.0f : 10.0f), -100.0f, 100.0f); + float decreaseSpeed = -5.0f; + float increaseSpeed = 10.0f; + float oxygenlowResistance = GetResistance(oxygenLowAffliction.Prefab); + decreaseSpeed *= (1f - oxygenlowResistance); + increaseSpeed *= (1f + oxygenlowResistance); + OxygenAmount = MathHelper.Clamp(OxygenAmount + deltaTime * (Character.OxygenAvailable < InsufficientOxygenThreshold ? decreaseSpeed : increaseSpeed), -100.0f, 100.0f); } UpdateOxygenProjSpecific(prevOxygen, deltaTime); @@ -807,8 +827,6 @@ namespace Barotrauma Vitality = MaxVitality; if (Unkillable || Character.GodMode) { return; } - float damageResistanceMultiplier = 1f - GetResistance("damage"); - foreach (LimbHealth limbHealth in limbHealths) { foreach (Affliction affliction in limbHealth.Afflictions) @@ -824,7 +842,6 @@ namespace Barotrauma { vitalityDecrease *= limbHealth.VitalityTypeMultipliers[type]; } - vitalityDecrease *= damageResistanceMultiplier; Vitality -= vitalityDecrease; affliction.CalculateDamagePerSecond(vitalityDecrease); } @@ -833,7 +850,6 @@ namespace Barotrauma foreach (Affliction affliction in afflictions) { float vitalityDecrease = affliction.GetVitalityDecrease(this); - vitalityDecrease *= damageResistanceMultiplier; Vitality -= vitalityDecrease; affliction.CalculateDamagePerSecond(vitalityDecrease); } @@ -951,13 +967,13 @@ namespace Barotrauma /// A dictionary where the key is the identifier of the item and the value the suitability /// If true, the suitability values are normalized between 0 and 1. If not, they're arbitrary values defined in the medical item XML, where negative values are unsuitable, and positive ones suitable. /// Amount of randomization to apply to the values (0 = the values are accurate, 1 = the values are completely random) - public void GetSuitableTreatments(Dictionary treatmentSuitability, bool normalize, float randomization = 0.0f) + public void GetSuitableTreatments(Dictionary treatmentSuitability, bool normalize, Limb limb = null, float randomization = 0.0f) { //key = item identifier //float = suitability treatmentSuitability.Clear(); float minSuitability = -10, maxSuitability = 10; - foreach (Affliction affliction in GetAllAfflictions()) + foreach (Affliction affliction in getAfflictions(limb)) { if (affliction.Strength < affliction.Prefab.TreatmentThreshold) { continue; } foreach (KeyValuePair treatment in affliction.Prefab.TreatmentSuitability) @@ -990,6 +1006,18 @@ namespace Barotrauma treatmentSuitability[treatment] += Rand.Range(-100.0f, 100.0f) * randomization; } } + + IEnumerable getAfflictions(Limb limb) + { + if (limb == null) + { + return GetAllAfflictions(); + } + else + { + return GetMatchingAfflictions(GetMatchingLimbHealth(limb)); + } + } } private readonly List activeAfflictions = new List(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs index d8cd67162..2841d305e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs @@ -89,11 +89,11 @@ namespace Barotrauma return (skill == null) ? 0.0f : skill.Level; } - public void IncreaseSkillLevel(string skillIdentifier, float increase) + public void IncreaseSkillLevel(string skillIdentifier, float increase, bool increasePastMax) { if (skills.TryGetValue(skillIdentifier, out Skill skill)) { - skill.Level += increase; + skill.IncreaseSkill(increase, increasePastMax); } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs index f439cefff..f3a502403 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs @@ -7,11 +7,18 @@ namespace Barotrauma private float level; public string Identifier { get; } + + public const float MaximumSkill = 100.0f; public float Level { get { return level; } - set { level = MathHelper.Clamp(value, 0.0f, 100.0f); } + set { level = value; } + } + + public void IncreaseSkill(float value, bool increasePastMax) + { + level = MathHelper.Clamp(level + value, 0.0f, increasePastMax ? float.MaxValue : MaximumSkill); } private Sprite icon; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index ed33edcd9..9afd742bb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -868,7 +868,7 @@ namespace Barotrauma /// public bool UpdateAttack(float deltaTime, Vector2 attackSimPos, IDamageable damageTarget, out AttackResult attackResult, float distance = -1, Limb targetLimb = null) { - attackResult = default(AttackResult); + attackResult = default; Vector2 simPos = ragdoll.SimplePhysicsEnabled ? character.SimPosition : SimPosition; float dist = distance > -1 ? distance : ConvertUnits.ToDisplayUnits(Vector2.Distance(simPos, attackSimPos)); bool wasRunning = attack.IsRunning; @@ -971,7 +971,7 @@ namespace Barotrauma wasHit = damageTarget != null; } - if (wasHit) + if (wasHit || attack.HitDetectionType == HitDetection.None) { if (character == Character.Controlled || GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) { @@ -1132,10 +1132,7 @@ namespace Barotrauma if (statusEffect.type != actionType) { continue; } if (statusEffect.type == ActionType.OnDamaged) { - if (statusEffect.AllowedAfflictions != null && (character.LastDamage.Afflictions == null || character.LastDamage.Afflictions.None(a => statusEffect.AllowedAfflictions.Contains(a.Prefab.AfflictionType) || statusEffect.AllowedAfflictions.Contains(a.Prefab.Identifier)))) - { - continue; - } + if (!statusEffect.HasRequiredAfflictions(character.LastDamage)) { continue; } if (statusEffect.OnlyPlayerTriggered) { if (character.LastAttacker == null || !character.LastAttacker.IsPlayer) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs index 2a80e9122..3288b0b7a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs @@ -15,7 +15,7 @@ namespace Barotrauma.Abilities private readonly string itemIdentifier; private readonly string[] tags; - private WeaponType weapontype; + private readonly WeaponType weapontype; public AbilityConditionAttackData(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { itemIdentifier = conditionElement.GetAttributeString("itemidentifier", ""); @@ -33,7 +33,7 @@ namespace Barotrauma.Abilities protected override bool MatchesConditionSpecific(object abilityData) { - if (abilityData is AttackData attackData) + if (abilityData is AbilityAttackData attackData) { Item item = attackData?.SourceAttack?.SourceItem; @@ -71,7 +71,7 @@ namespace Barotrauma.Abilities } else { - LogAbilityConditionError(abilityData, typeof(AttackData)); + LogAbilityConditionError(abilityData, typeof(AbilityAttackData)); return false; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackResult.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackResult.cs index 909c694f2..ac9eb34df 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackResult.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackResult.cs @@ -30,7 +30,7 @@ namespace Barotrauma.Abilities } else { - LogAbilityConditionError(abilityData, typeof(AttackData)); + LogAbilityConditionError(abilityData, typeof(AbilityAttackData)); return false; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionData.cs index d80a6257d..5135b0924 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionData.cs @@ -1,7 +1,4 @@ -using Microsoft.Xna.Framework; -using System; -using System.Collections.Generic; -using System.Linq; +using System; using System.Xml.Linq; namespace Barotrauma.Abilities diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionHandsomeStranger.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionHandsomeStranger.cs deleted file mode 100644 index 55ee4fa06..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionHandsomeStranger.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Xml.Linq; - -namespace Barotrauma.Abilities -{ - class AbilityConditionHandsomeStranger : AbilityConditionData - { - string skillIdentifier; - - public AbilityConditionHandsomeStranger(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) - { - skillIdentifier = conditionElement.GetAttributeString("skillidentifier", "").ToLowerInvariant(); - } - - protected override bool MatchesConditionSpecific(object abilityData) - { - if (abilityData is string skillIdentifier) - { - return this.skillIdentifier == skillIdentifier; - } - else - { - LogAbilityConditionError(abilityData, typeof(string)); - return false; - } - } - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionIsAiming.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionIsAiming.cs new file mode 100644 index 000000000..324676a98 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionIsAiming.cs @@ -0,0 +1,54 @@ +using Barotrauma.Items.Components; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionIsAiming : AbilityConditionDataless + { + private enum WeaponType + { + Any = 0, + Melee = 1, + Ranged = 2 + }; + + private WeaponType weapontype; + public AbilityConditionIsAiming(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + switch (conditionElement.GetAttributeString("weapontype", "")) + { + case "melee": + weapontype = WeaponType.Melee; + break; + case "ranged": + weapontype = WeaponType.Ranged; + break; + } + } + + protected override bool MatchesConditionSpecific() + { + bool aimingCorrectItem = false; + if (character.AnimController is HumanoidAnimController animController) + { + foreach (Item item in character.HeldItems) + { + switch (weapontype) + { + case WeaponType.Melee: + aimingCorrectItem |= item.GetComponent() != null && animController.IsAimingMelee; + break; + case WeaponType.Ranged: + aimingCorrectItem |= item.GetComponent() != null && animController.IsAiming; + break; + default: + aimingCorrectItem |= animController.IsAiming || animController.IsAimingMelee; + break; + } + } + } + + return aimingCorrectItem; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs index cfa1db21c..4e6b530cd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs @@ -22,10 +22,9 @@ namespace Barotrauma.Abilities { item = tempItem.Prefab; } - // this and other instances of this type of casting will be refactored - else if (abilityData is (ItemPrefab itemPrefab, object _)) + else if (abilityData is IAbilityItemPrefab abilityItemPrefab) { - item = itemPrefab; + item = abilityItemPrefab.ItemPrefab; } if (item != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionReduceAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionReduceAffliction.cs index 24044dc0e..888217b9f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionReduceAffliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionReduceAffliction.cs @@ -15,17 +15,17 @@ namespace Barotrauma.Abilities protected override bool MatchesConditionSpecific(object abilityData) { - if (abilityData is (Affliction affliction, float reduceAmount)) + if (abilityData is IAbilityAffliction abilityAffliction) { - if (allowedTypes.Find(c => c == affliction.Prefab.AfflictionType) == null) { return false; } + if (allowedTypes.Find(c => c == abilityAffliction.Affliction.Prefab.AfflictionType) == null) { return false; } - if (!string.IsNullOrEmpty(identifier) && affliction.Prefab.Identifier != identifier) { return false; } + if (!string.IsNullOrEmpty(identifier) && abilityAffliction.Affliction.Prefab.Identifier != identifier) { return false; } return true; } else { - LogAbilityConditionError(abilityData, typeof((Affliction, float))); + LogAbilityConditionError(abilityData, typeof(IAbilityAffliction)); return false; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionSkill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionSkill.cs new file mode 100644 index 000000000..37fd19923 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionSkill.cs @@ -0,0 +1,32 @@ +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionSkill : AbilityConditionData + { + private readonly string skillIdentifier; + + public AbilityConditionSkill(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + skillIdentifier = conditionElement.GetAttributeString("skillidentifier", "").ToLowerInvariant(); + } + + private bool MatchesConditionSpecific(string skillIdentifier) + { + return this.skillIdentifier == skillIdentifier; + } + + protected override bool MatchesConditionSpecific(object abilityData) + { + if ((abilityData as string ?? (abilityData as IAbilityString)?.String) is string skillIdentifier) + { + return MatchesConditionSpecific(skillIdentifier); + } + else + { + LogAbilityConditionError(abilityData, typeof(string)); + return false; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAboveVitality.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAboveVitality.cs index 6543c7b32..9fe8fa1a4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAboveVitality.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAboveVitality.cs @@ -1,11 +1,10 @@ -using System.Linq; -using System.Xml.Linq; +using System.Xml.Linq; namespace Barotrauma.Abilities { class AbilityConditionAboveVitality : AbilityConditionDataless { - float vitalityPercentage; + private readonly float vitalityPercentage; public AbilityConditionAboveVitality(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCoauthor.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCoauthor.cs new file mode 100644 index 000000000..7525427eb --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCoauthor.cs @@ -0,0 +1,26 @@ +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionCoauthor : AbilityConditionDataless + { + private readonly string jobIdentifier; + + public AbilityConditionCoauthor(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + jobIdentifier = conditionElement.GetAttributeString("jobidentifier", string.Empty); + } + + protected override bool MatchesConditionSpecific() + { + if (character.SelectedCharacter is Character otherCharacter) + { + if (!otherCharacter.HasJob(jobIdentifier)) { return false; } + if (!(character.SelectedBy == otherCharacter)) { return false; } + return true; + } + return false; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionDataless.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionDataless.cs index 023fe029f..de8bccced 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionDataless.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionDataless.cs @@ -1,8 +1,4 @@ -using Microsoft.Xna.Framework; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; +using System.Xml.Linq; namespace Barotrauma.Abilities { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs new file mode 100644 index 000000000..0fee25880 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs @@ -0,0 +1,23 @@ +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionHasPermanentStat : AbilityConditionDataless + { + private readonly StatTypes statType; + private readonly float min; + + public AbilityConditionHasPermanentStat(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + statType = CharacterAbilityGroup.ParseStatType(conditionElement.GetAttributeString("stattype", ""), characterTalent.DebugIdentifier); + min = conditionElement.GetAttributeFloat("min", 0f); + } + + protected override bool MatchesConditionSpecific() + { + // should consider decoupling this from stat values entirely + return character.Info.GetSavedStatValue(statType) >= min; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasStatusTag.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasStatusTag.cs new file mode 100644 index 000000000..2a22f2098 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasStatusTag.cs @@ -0,0 +1,31 @@ +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionHasStatusTag : AbilityConditionDataless + { + private readonly string tag; + + + public AbilityConditionHasStatusTag(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + tag = conditionElement.GetAttributeString("tag", ""); + if (string.IsNullOrEmpty(tag)) + { + DebugConsole.AddWarning($"Error in talent \"{characterTalent.Prefab.OriginalName}\" - tag not defined in AbilityConditionHasStatusTag."); + } + } + + protected override bool MatchesConditionSpecific() + { + if (!string.IsNullOrEmpty(tag)) + { + return + StatusEffect.DurationList.Any(d => d.Targets.Contains(character) && d.Parent.HasTag(tag)) || + DelayedEffect.DelayList.Any(d => d.Targets.Contains(character) && d.Parent.HasTag(tag)); + } + return false; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInFriendlySubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInFriendlySubmarine.cs new file mode 100644 index 000000000..28f27ed9b --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInFriendlySubmarine.cs @@ -0,0 +1,15 @@ + +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionInFriendlySubmarine : AbilityConditionDataless + { + public AbilityConditionInFriendlySubmarine(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { } + + protected override bool MatchesConditionSpecific() + { + return character.Submarine?.TeamID == character.TeamID; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInHull.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInHull.cs new file mode 100644 index 000000000..e08291e6b --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionInHull.cs @@ -0,0 +1,15 @@ + +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionInHull : AbilityConditionDataless + { + public AbilityConditionInHull(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { } + + protected override bool MatchesConditionSpecific() + { + return character.CurrentHull != null; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionLevelsBehindHighest.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionLevelsBehindHighest.cs new file mode 100644 index 000000000..f2c4b2fb7 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionLevelsBehindHighest.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionLevelsBehindHighest : AbilityConditionDataless + { + private readonly int levelsBehind; + public AbilityConditionLevelsBehindHighest(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + levelsBehind = conditionElement.GetAttributeInt("levelsbehind", 0); + } + + protected override bool MatchesConditionSpecific() + { + return Character.GetFriendlyCrew(character).Where(c => c.Info != null && (c.Info.GetCurrentLevel() - character.Info.GetCurrentLevel() >= levelsBehind)).Any(); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionMission.cs index f27ecf4c1..29cab2f45 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionMission.cs @@ -24,13 +24,13 @@ namespace Barotrauma.Abilities protected override bool MatchesConditionSpecific(object abilityData) { - if (abilityData is (Mission mission, AbilityValue missionAbilityValue)) + if (abilityData is IAbilityMission abilityMission) { - return mission.Prefab.Type == missionType; + return abilityMission.Mission.Prefab.Type == missionType; } else { - LogAbilityConditionError(abilityData, typeof((Mission, AbilityValue))); + LogAbilityConditionError(abilityData, typeof(IAbilityMission)); return false; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionServerRandom.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionServerRandom.cs index 3cc8ae4f5..5b582f799 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionServerRandom.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionServerRandom.cs @@ -1,14 +1,10 @@ -using Microsoft.Xna.Framework; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Xml.Linq; +using System.Xml.Linq; namespace Barotrauma.Abilities { class AbilityConditionServerRandom : AbilityConditionDataless { - private float randomChance = 0f; + private readonly float randomChance = 0f; public override bool AllowClientSimulation => false; public AbilityConditionServerRandom(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityInterfaces.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityInterfaces.cs new file mode 100644 index 000000000..46478b7b7 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityInterfaces.cs @@ -0,0 +1,32 @@ +namespace Barotrauma.Abilities +{ + interface IAbilityItemPrefab + { + public ItemPrefab ItemPrefab { get; set; } + } + + interface IAbilityValue + { + public float Value { get; set; } + } + + interface IAbilityMission + { + public Mission Mission { get; set; } + } + + interface IAbilityCharacter + { + public Character Character { get; set; } + } + + interface IAbilityString + { + public string String { get; set; } + } + + interface IAbilityAffliction + { + public Affliction Affliction { get; set; } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityObjects.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityObjects.cs new file mode 100644 index 000000000..9247ad159 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityObjects.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; + +namespace Barotrauma.Abilities +{ + + class AbilityValue : IAbilityValue + { + public AbilityValue(float value) + { + Value = value; + } + public float Value { get; set; } + } + + class AbilityValueItem : IAbilityValue, IAbilityItemPrefab + { + public AbilityValueItem(float value, ItemPrefab itemPrefab) + { + Value = value; + ItemPrefab = itemPrefab; + } + public float Value { get; set; } + public ItemPrefab ItemPrefab { get; set; } + } + + class AbilityValueString : IAbilityValue, IAbilityString + { + public AbilityValueString(float value, string abilityString) + { + Value = value; + String = abilityString; + } + public float Value { get; set; } + public string String { get; set; } + } + + class AbilityStringCharacter : IAbilityCharacter, IAbilityString + { + public AbilityStringCharacter(string abilityString, Character character) + { + String = abilityString; + Character = character; + } + public Character Character { get; set; } + public string String { get; set; } + } + + class AbilityValueAffliction : IAbilityValue, IAbilityAffliction + { + public AbilityValueAffliction(float value, Affliction affliction) + { + Value = value; + Affliction = affliction; + } + public float Value { get; set; } + public Affliction Affliction { get; set; } + } + + class AbilityValueMission : IAbilityValue, IAbilityMission + { + public AbilityValueMission(float value, Mission mission) + { + Value = value; + Mission = mission; + } + public float Value { get; set; } + public Mission Mission { get; set; } + } + + class AbilityAttackData : IAbilityCharacter + { + public float DamageMultiplier { get; set; } = 1f; + public float AddedPenetration { get; set; } = 0f; + public List Afflictions { get; set; } + public Attack SourceAttack { get; } + public Character Character { get; set; } + public Character Attacker { get; set; } + + public AbilityAttackData(Attack sourceAttack, Character character) + { + SourceAttack = sourceAttack; + Character = character; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs index 739e7ced1..9b4414901 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs @@ -12,12 +12,16 @@ namespace Barotrauma.Abilities public CharacterTalent CharacterTalent { get; } public Character Character { get; } - public virtual bool RequiresAlive => true; + public bool RequiresAlive { get; } + public virtual bool AllowClientSimulation => false; public virtual bool AppliesEffectOnIntervalUpdate => false; private const float DefaultEffectTime = 1.0f; + // currently resets if the character dies. would need to be stored in a dictionary of sorts to maintain through death + + /// /// Used primarily for StatusEffects. Default to constant outside interval abilities. /// @@ -28,6 +32,7 @@ namespace Barotrauma.Abilities CharacterAbilityGroup = characterAbilityGroup; CharacterTalent = characterAbilityGroup.CharacterTalent; Character = CharacterTalent.Character; + RequiresAlive = abilityElement.GetAttributeBool("requiresalive", true); } public bool IsViable() @@ -132,11 +137,5 @@ namespace Barotrauma.Abilities } return flagType; } - - public static float DistanceToSquaredDistance(float distance) - { - return distance * distance; - } - } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs index e370e94b3..773992306 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs @@ -10,16 +10,41 @@ namespace Barotrauma.Abilities protected readonly List statusEffects; + private readonly bool applyToSelected; + + readonly List targets = new List(); + public CharacterAbilityApplyStatusEffects(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) { statusEffects = CharacterAbilityGroup.ParseStatusEffects(CharacterTalent, abilityElement.GetChildElement("statuseffects")); + applyToSelected = abilityElement.GetAttributeBool("applytoselected", false); } protected void ApplyEffectSpecific(Character targetCharacter) { foreach (var statusEffect in statusEffects) { - statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, targetCharacter); + if (statusEffect.HasTargetType(StatusEffect.TargetType.UseTarget)) + { + // currently used this to spawn items on the targeted character + statusEffect.SetUser(targetCharacter); + statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, targetCharacter, targetCharacter); + } + else if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) + { + targets.Clear(); + targets.AddRange(statusEffect.GetNearbyTargets(targetCharacter.WorldPosition, targets)); + statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, targetCharacter, targets); + } + else if (statusEffect.HasTargetType(StatusEffect.TargetType.This)) + { + statusEffect.SetUser(Character); + statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, Character); + } + else + { + statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, targetCharacter); + } } } @@ -30,13 +55,17 @@ namespace Barotrauma.Abilities protected override void ApplyEffect(object abilityData) { - if (abilityData is Character targetCharacter) + if (applyToSelected && Character.SelectedCharacter is Character selectedCharacter) + { + ApplyEffectSpecific(selectedCharacter); + } + else if ((abilityData as Character ?? (abilityData as IAbilityCharacter)?.Character) is Character targetCharacter) { ApplyEffectSpecific(targetCharacter); } - else + else { - ApplyEffect(); + ApplyEffect(); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAllies.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAllies.cs new file mode 100644 index 000000000..2b1115068 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAllies.cs @@ -0,0 +1,30 @@ +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityApplyStatusEffectsToAllies : CharacterAbilityApplyStatusEffects + { + private readonly bool allowSelf; + + public CharacterAbilityApplyStatusEffectsToAllies(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + allowSelf = abilityElement.GetAttributeBool("allowself", true); + } + + + protected override void ApplyEffect() + { + IEnumerable chosenCharacters = Character.GetFriendlyCrew(Character).Where(c => allowSelf || c != Character); + + foreach (Character character in chosenCharacters) + { + ApplyEffectSpecific(character); + } + } + + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAttacker.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAttacker.cs new file mode 100644 index 000000000..1f346accd --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAttacker.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityApplyStatusEffectsToAttacker : CharacterAbilityApplyStatusEffects + { + public CharacterAbilityApplyStatusEffectsToAttacker(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + } + + protected override void ApplyEffect(object abilityData) + { + if ((abilityData as AbilityAttackData)?.Attacker is Character attacker) + { + ApplyEffectSpecific(attacker); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToNearestAlly.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToNearestAlly.cs index a12622816..a0701c782 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToNearestAlly.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToNearestAlly.cs @@ -1,6 +1,5 @@ using Microsoft.Xna.Framework; -using System.Collections.Generic; -using System.Linq; +using System; using System.Xml.Linq; namespace Barotrauma.Abilities @@ -10,7 +9,7 @@ namespace Barotrauma.Abilities protected float squaredMaxDistance; public CharacterAbilityApplyStatusEffectsToNearestAlly(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) { - squaredMaxDistance = DistanceToSquaredDistance(abilityElement.GetAttributeFloat("maxdistance", float.MaxValue)); + squaredMaxDistance = MathF.Pow(abilityElement.GetAttributeFloat("maxdistance", float.MaxValue), 2); } protected override void ApplyEffect() @@ -20,7 +19,7 @@ namespace Barotrauma.Abilities foreach (Character crewCharacter in Character.GetFriendlyCrew(Character)) { - if (crewCharacter != Character && Vector2.DistanceSquared(Character.SimPosition, Character.GetRelativeSimPosition(crewCharacter)) is float tempDistance && tempDistance < closestDistance) + if (crewCharacter != Character && Vector2.DistanceSquared(Character.WorldPosition, crewCharacter.WorldPosition) is float tempDistance && tempDistance < closestDistance) { closestCharacter = crewCharacter; closestDistance = tempDistance; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToRandomAlly.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToRandomAlly.cs index 8fe1ea29d..0f1fd20b2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToRandomAlly.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToRandomAlly.cs @@ -1,5 +1,6 @@ using Barotrauma.Extensions; using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; @@ -16,7 +17,7 @@ namespace Barotrauma.Abilities public CharacterAbilityApplyStatusEffectsToRandomAlly(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) { - squaredMaxDistance = DistanceToSquaredDistance(abilityElement.GetAttributeFloat("maxdistance", float.MaxValue)); + squaredMaxDistance = MathF.Pow(abilityElement.GetAttributeFloat("maxdistance", float.MaxValue), 2); allowDifferentSub = abilityElement.GetAttributeBool("mustbeonsamesub", true); allowSelf = abilityElement.GetAttributeBool("allowself", true); } @@ -26,9 +27,9 @@ namespace Barotrauma.Abilities Character chosenCharacter = null; chosenCharacter = Character.GetFriendlyCrew(Character).Where(c => - (allowSelf ||c != Character) && + (allowSelf || c != Character) && (allowDifferentSub || c.Submarine == Character.Submarine) && - Vector2.DistanceSquared(Character.SimPosition, Character.GetRelativeSimPosition(c)) is float tempDistance && + Vector2.DistanceSquared(Character.WorldPosition, c.WorldPosition) is float tempDistance && tempDistance < squaredMaxDistance).GetRandom(); if (chosenCharacter == null) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMoney.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMoney.cs index 86e1cb093..4ac33d7c7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMoney.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveMoney.cs @@ -7,16 +7,41 @@ namespace Barotrauma.Abilities { public override bool AppliesEffectOnIntervalUpdate => true; - private int amount; + private readonly int amount; + private StatTypes scalingStatType; public CharacterAbilityGiveMoney(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) { amount = abilityElement.GetAttributeInt("amount", 0); + scalingStatType = CharacterAbilityGroup.ParseStatType(abilityElement.GetAttributeString("scalingstattype", "None"), CharacterTalent.DebugIdentifier); + } + + private void ApplyEffectSpecific(Character targetCharacter) + { + float multiplier = 1f; + if (scalingStatType != StatTypes.None) + { + multiplier = 0 + Character.Info.GetSavedStatValue(scalingStatType); + } + + targetCharacter.GiveMoney((int)(multiplier * amount)); + } + + protected override void ApplyEffect(object abilityData) + { + if ((abilityData as Character ?? (abilityData as IAbilityCharacter)?.Character) is Character targetCharacter) + { + ApplyEffectSpecific(targetCharacter); + } + else + { + ApplyEffectSpecific(Character); + } } protected override void ApplyEffect() { - Character.GiveMoney(amount); + ApplyEffectSpecific(Character); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs index 2a8797b4e..c8147397c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs @@ -8,8 +8,10 @@ namespace Barotrauma.Abilities private readonly string statIdentifier; private readonly StatTypes statType; private readonly float value; + private readonly float maxValue; private readonly bool targetAllies; private readonly bool removeOnDeath; + private readonly bool removeAfterRound; //private readonly float maximumValue; public override bool AppliesEffectOnIntervalUpdate => true; @@ -19,8 +21,10 @@ namespace Barotrauma.Abilities statIdentifier = abilityElement.GetAttributeString("statidentifier", "").ToLowerInvariant(); statType = CharacterAbilityGroup.ParseStatType(abilityElement.GetAttributeString("stattype", ""), CharacterTalent.DebugIdentifier); value = abilityElement.GetAttributeFloat("value", 0f); + maxValue = abilityElement.GetAttributeFloat("maxvalue", float.MaxValue); targetAllies = abilityElement.GetAttributeBool("targetallies", false); removeOnDeath = abilityElement.GetAttributeBool("removeondeath", true); + removeAfterRound = abilityElement.GetAttributeBool("removeafterround", false); //maximumValue = abilityElement.GetAttributeFloat("maximumvalue", float.MaxValue); } @@ -38,11 +42,11 @@ namespace Barotrauma.Abilities { if (targetAllies) { - Character.GetFriendlyCrew(Character).ForEach(c => c?.Info.ChangeSavedStatValue(statType, value, statIdentifier, removeOnDeath)); + Character.GetFriendlyCrew(Character).ForEach(c => c?.Info.ChangeSavedStatValue(statType, value, statIdentifier, removeOnDeath, removeAfterRound, maxValue)); } else { - Character?.Info.ChangeSavedStatValue(statType, value, statIdentifier, removeOnDeath); + Character?.Info.ChangeSavedStatValue(statType, value, statIdentifier, removeOnDeath, removeAfterRound, maxValue); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveResistance.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveResistance.cs index 5d9cd7fee..707d31449 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveResistance.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveResistance.cs @@ -4,18 +4,23 @@ namespace Barotrauma.Abilities { class CharacterAbilityGiveResistance : CharacterAbility { - private string resistanceId; - private float resistance; + private readonly string resistanceId; + private readonly float multiplier; public CharacterAbilityGiveResistance(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) { resistanceId = abilityElement.GetAttributeString("resistanceid", ""); - resistance = abilityElement.GetAttributeFloat("resistance", 1f); + multiplier = abilityElement.GetAttributeFloat("multiplier", 1f); + + if (string.IsNullOrEmpty(resistanceId)) + { + DebugConsole.ThrowError("Error in CharacterAbilityGiveResistance - resistance identifier not set."); + } } public override void InitializeAbility(bool addingFirstTime) { - Character.ChangeAbilityResistance(resistanceId, resistance); + Character.ChangeAbilityResistance(resistanceId, multiplier); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveStat.cs index 3b55618d7..c999d3999 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveStat.cs @@ -4,10 +4,9 @@ namespace Barotrauma.Abilities { class CharacterAbilityGiveStat : CharacterAbility { - private StatTypes statType; - private float value; + private readonly StatTypes statType; + private readonly float value; - // this and resistance giving should probably be moved directly to charactertalent attributes, as they don't need to interact with either ability group types public CharacterAbilityGiveStat(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) { statType = CharacterAbilityGroup.ParseStatType(abilityElement.GetAttributeString("stattype", ""), CharacterTalent.DebugIdentifier); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAttackData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAttackData.cs index 6ee9dc1dc..1d32acc85 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAttackData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAttackData.cs @@ -7,8 +7,9 @@ namespace Barotrauma.Abilities { private readonly List afflictions; - float addedDamageMultiplier; - float addedPenetration; + private readonly float addedDamageMultiplier; + private readonly float addedPenetration; + private readonly bool implode; public CharacterAbilityModifyAttackData(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) { @@ -18,11 +19,12 @@ namespace Barotrauma.Abilities } addedDamageMultiplier = abilityElement.GetAttributeFloat("addeddamagemultiplier", 0f); addedPenetration = abilityElement.GetAttributeFloat("addedpenetration", 0f); + implode = abilityElement.GetAttributeBool("implode", false); } protected override void ApplyEffect(object abilityData) { - if (abilityData is AttackData attackData) + if (abilityData is AbilityAttackData attackData) { if (attackData.Afflictions == null) { @@ -34,6 +36,13 @@ namespace Barotrauma.Abilities } attackData.DamageMultiplier += addedDamageMultiplier; attackData.AddedPenetration += addedPenetration; + + if (implode) + { + // might have issues, as the method used to be private and only used for pressure death + attackData.Character?.Implode(); + } + } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyResistance.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyResistance.cs index 4e44403d7..4317f6745 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyResistance.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyResistance.cs @@ -4,8 +4,8 @@ namespace Barotrauma.Abilities { class CharacterAbilityModifyResistance : CharacterAbility { - private string resistanceId; - private float resistance; + private readonly string resistanceId; + private readonly float resistance; bool lastState; // should probably be split to different classes @@ -13,6 +13,11 @@ namespace Barotrauma.Abilities { resistanceId = abilityElement.GetAttributeString("resistanceid", ""); resistance = abilityElement.GetAttributeFloat("resistance", 1f); + + if (string.IsNullOrEmpty(resistanceId)) + { + DebugConsole.ThrowError("Error in CharacterAbilityModifyResistance - resistance identifier not set."); + } } public override void UpdateCharacterAbility(bool conditionsMatched, float timeSinceLastUpdate) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToSkill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToSkill.cs new file mode 100644 index 000000000..44435e95d --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyStatToSkill.cs @@ -0,0 +1,52 @@ +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityModifyStatToSkill : CharacterAbility + { + private readonly StatTypes statType; + private readonly float maxValue; + private readonly string skillIdentifier; + private readonly bool useAll; + private float lastValue = 0f; + + public CharacterAbilityModifyStatToSkill(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + statType = CharacterAbilityGroup.ParseStatType(abilityElement.GetAttributeString("stattype", ""), CharacterTalent.DebugIdentifier); + maxValue = abilityElement.GetAttributeFloat("maxvalue", 0f); + skillIdentifier = abilityElement.GetAttributeString("skillidentifier", string.Empty); + useAll = skillIdentifier == "all"; + } + + protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate) + { + Character.ChangeStat(statType, -lastValue); + + if (conditionsMatched) + { + float skillTotal = 0f; + + if (useAll && Character.Info?.Job != null) + { + foreach (Skill skill in Character.Info.Job.Skills) + { + skillTotal += Character.GetSkillLevel(skill.Identifier); + } + skillTotal /= Character.Info.Job.Skills.Count; + } + else + { + skillTotal = Character.GetSkillLevel(skillIdentifier); + } + + lastValue = skillTotal / 100f * maxValue; + Character.ChangeStat(statType, lastValue); + } + else + { + lastValue = 0f; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyValue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyValue.cs index 7f27c334f..b4e15f0b3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyValue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyValue.cs @@ -5,42 +5,21 @@ namespace Barotrauma.Abilities class CharacterAbilityModifyValue : CharacterAbility { private float addedValue; - private float multiplierValue; + private float multiplyValue; public CharacterAbilityModifyValue(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) { addedValue = abilityElement.GetAttributeFloat("addedvalue", 0f); - multiplierValue = abilityElement.GetAttributeFloat("multipliervalue", 1f); + multiplyValue = abilityElement.GetAttributeFloat("multiplyvalue", 1f); } protected override void ApplyEffect(object abilityData) { - if (abilityData is AbilityValue abilityValue) + if (abilityData is IAbilityValue abilityValue) { - ApplyEffectSpecific(abilityValue); + abilityValue.Value += addedValue; + abilityValue.Value *= multiplyValue; } - else if (abilityData is (object _, AbilityValue tupleAbilityValue)) - { - ApplyEffectSpecific(tupleAbilityValue); - } - } - - private void ApplyEffectSpecific(AbilityValue abilityValue) - { - abilityValue.Value += addedValue; - abilityValue.Value *= multiplierValue; - } - - } - - // this seems like a real silly way to have to pass values by reference into these same interfaces - // if more of these are required, maybe there should be an additional set of interfaces to easily pass values by reference instead - class AbilityValue - { - public float Value { get; set; } - public AbilityValue(float value) - { - Value = value; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityResetPermanentStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityResetPermanentStat.cs index eb57809e5..06706568c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityResetPermanentStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityResetPermanentStat.cs @@ -5,7 +5,7 @@ namespace Barotrauma.Abilities class CharacterAbilityResetPermanentStat : CharacterAbility { private readonly string statIdentifier; - public override bool RequiresAlive => false; + public override bool AppliesEffectOnIntervalUpdate => true; public CharacterAbilityResetPermanentStat(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityRevive.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityRevive.cs new file mode 100644 index 000000000..15772796c --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityRevive.cs @@ -0,0 +1,29 @@ +using Microsoft.Xna.Framework; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityRevive : CharacterAbility + { + public override bool AppliesEffectOnIntervalUpdate => true; + + public CharacterAbilityRevive(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + } + + private void ApplyEffectSpecific() + { + Character.Revive(); + } + + protected override void ApplyEffect() + { + ApplyEffectSpecific(); + } + + protected override void ApplyEffect(object abilityData) + { + ApplyEffectSpecific(); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAlienHoarder.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAlienHoarder.cs new file mode 100644 index 000000000..9b498a085 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAlienHoarder.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityAlienHoarder : CharacterAbility + { + private readonly float addedDamageMultiplierPerItem; + private readonly int maxAmount; + private readonly string[] tags; + + public CharacterAbilityAlienHoarder(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + addedDamageMultiplierPerItem = abilityElement.GetAttributeFloat("addeddamagemultiplierperitem", 0f); + maxAmount = abilityElement.GetAttributeInt("maxamount", 0); + tags = abilityElement.GetAttributeStringArray("tags", Array.Empty(), convertToLowerInvariant: true); + } + + protected override void ApplyEffect(object abilityData) + { + if (abilityData is AbilityAttackData attackData) + { + float totalAddedDamageMultiplier = 0f; + foreach (Item item in Character.Inventory.AllItems) + { + if (tags.Any(t => item.Prefab.Tags.Any(p => t == p))) + { + totalAddedDamageMultiplier += addedDamageMultiplierPerItem; + } + } + attackData.DamageMultiplier += addedDamageMultiplierPerItem; + } + else + { + LogAbilityDataMismatch(); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityApprenticeship.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityApprenticeship.cs index 808ca4b0b..8196229ff 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityApprenticeship.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityApprenticeship.cs @@ -12,9 +12,9 @@ namespace Barotrauma.Abilities protected override void ApplyEffect(object abilityData) { - if (abilityData is (string skillIdentifier, Character character) && character != Character) + if (abilityData is AbilityStringCharacter abilityStringCharacter && abilityStringCharacter.Character != Character) { - character.Info?.IncreaseSkillLevel(skillIdentifier, 1.0f, character.Position + Vector2.UnitY * 175.0f); + Character.Info?.IncreaseSkillLevel(abilityStringCharacter.String, 1.0f, abilityStringCharacter.Character.Position + Vector2.UnitY * 175.0f); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityByTheBook.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityByTheBook.cs new file mode 100644 index 000000000..de83bf9a9 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityByTheBook.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class CharacterAbilityByTheBook : CharacterAbility + { + private int moneyAmount; + private int max; + + public CharacterAbilityByTheBook(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + moneyAmount = abilityElement.GetAttributeInt("moneyamount", 0); + max = abilityElement.GetAttributeInt("max", 0); + } + + protected override void ApplyEffect() + { + IEnumerable enemyCharacters = Character.CharacterList.Where(c => c.TeamID == CharacterTeamType.None); + + int timesGiven = 0; + foreach (Character enemyCharacter in enemyCharacters) + { + if (!enemyCharacter.IsHuman) { continue; } + if (enemyCharacter.Submarine == null || enemyCharacter.Submarine != Submarine.MainSub) { continue; } + if (enemyCharacter.IsDead) { continue; } + if (!enemyCharacter.LockHands) { continue; } + if (timesGiven > max) { continue; } + Character.GiveMoney(moneyAmount); + timesGiven++; + } + + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityInsurancePolicy.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityInsurancePolicy.cs index 9241769e4..1583bd53b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityInsurancePolicy.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityInsurancePolicy.cs @@ -9,45 +9,22 @@ namespace Barotrauma.Abilities class CharacterAbilityInsurancePolicy : CharacterAbility { public override bool AppliesEffectOnIntervalUpdate => true; - public override bool RequiresAlive => false; - private readonly int moneyPerLevel; - private bool hasOccurred = false; + private readonly int moneyPerMission; private static List clientsAlreadyUsed = new List(); public CharacterAbilityInsurancePolicy(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) { - moneyPerLevel = abilityElement.GetAttributeInt("moneyperlevel", 0); + moneyPerMission = abilityElement.GetAttributeInt("moneypermission", 0); } protected override void ApplyEffect() { - if (Character?.Info is CharacterInfo info && !hasOccurred) + if (Character?.Info is CharacterInfo info) { - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) - { - foreach (Client client in GameMain.NetworkMember.ConnectedClients) - { - if (client.Character == Character && clientsAlreadyUsed.Contains(client)) { return; } - } - } - Character.GiveMoney(moneyPerLevel * info.GetCurrentLevel()); - hasOccurred = true; - - // this is an ugly way to do this, but this effect should not occur more than once per round for a client - // this seemed like the simplest way to do it since characters are instantiated from scratch each time - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) - { - foreach (Client client in GameMain.NetworkMember.ConnectedClients) - { - if (client.Character == Character) - { - clientsAlreadyUsed.Add(client); - } - } - } + Character.GiveMoney(moneyPerMission * info.MissionsCompletedSinceDeath); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityPsychoClown.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityPsychoClown.cs index 4e7ac7225..61e9d9cf6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityPsychoClown.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityPsychoClown.cs @@ -5,14 +5,14 @@ namespace Barotrauma.Abilities class CharacterAbilityPsychoClown : CharacterAbility { private StatTypes statType; - private float value; + private float maxValue; private string afflictionIdentifier; private float lastValue = 0f; public CharacterAbilityPsychoClown(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) { statType = CharacterAbilityGroup.ParseStatType(abilityElement.GetAttributeString("stattype", ""), CharacterTalent.DebugIdentifier); - value = abilityElement.GetAttributeFloat("value", 0f); + maxValue = abilityElement.GetAttributeFloat("maxvalue", 0f); afflictionIdentifier = abilityElement.GetAttributeString("afflictionidentifier", ""); } @@ -32,7 +32,7 @@ namespace Barotrauma.Abilities afflictionStrength = affliction.Strength / affliction.Prefab.MaxStrength; } - lastValue = afflictionStrength * value; + lastValue = afflictionStrength * maxValue; Character.ChangeStat(statType, lastValue); } else diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityRegenerateLoot.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityRegenerateLoot.cs index 5a2ff174e..cb82641f5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityRegenerateLoot.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityRegenerateLoot.cs @@ -8,6 +8,8 @@ namespace Barotrauma.Abilities { class CharacterAbilityRegenerateLoot : CharacterAbility { + // not maintained through death, so it's possible for players to respawn and re-loot chests + // seems like a minor issue for now List openedContainers = new List(); public CharacterAbilityRegenerateLoot(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityStonewall.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityStonewall.cs index 6597c6973..cc2aa51c0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityStonewall.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityStonewall.cs @@ -10,20 +10,20 @@ namespace Barotrauma.Abilities { private readonly List statusEffects; private readonly List statusEffectsReset; - private int maxEnemyCount; - private float squaredDistance; + 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 = DistanceToSquaredDistance(abilityElement.GetAttributeFloat("distance", 0)); + squaredDistance = MathF.Pow(abilityElement.GetAttributeFloat("distance", 0), 2); } protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate) { - int numberOfEnemiesInRange = Character.CharacterList.Where(c => !HumanAIController.IsFriendly(Character, c) && !c.IsDead && Vector2.DistanceSquared(Character.SimPosition, Character.GetRelativeSimPosition(c)) < squaredDistance).Count(); + int numberOfEnemiesInRange = Character.CharacterList.Count(c => !HumanAIController.IsFriendly(Character, c) && !c.IsDead && Vector2.DistanceSquared(Character.WorldPosition, c.WorldPosition) < squaredDistance); foreach (var statusEffect in statusEffectsReset) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTandemFire.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTandemFire.cs index 284ec4ae6..e3b3ba0c3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTandemFire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTandemFire.cs @@ -8,6 +8,7 @@ namespace Barotrauma.Abilities { class CharacterAbilityTandemFire : CharacterAbilityApplyStatusEffectsToNearestAlly { + // this should just be its own class, misleading to inherit here private string tag; public CharacterAbilityTandemFire(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs index c49fa439c..1c5471f24 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs @@ -14,6 +14,10 @@ namespace Barotrauma.Abilities // currently only used to turn off simulation if random conditions are in use public bool IsActive { get; private set; } = true; + protected int maxTriggerCount { get; } + protected int timesTriggered = 0; + + // add support for OR conditions? protected readonly List abilityConditions = new List(); @@ -24,7 +28,7 @@ namespace Barotrauma.Abilities { CharacterTalent = characterTalent; Character = CharacterTalent.Character; - + maxTriggerCount = abilityElementGroup.GetAttributeInt("maxtriggercount", int.MaxValue); foreach (XElement subElement in abilityElementGroup.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupEffect.cs index f49851390..184c5e8a9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupEffect.cs @@ -27,6 +27,7 @@ namespace Barotrauma.Abilities private bool IsApplicable(object abilityData) { + if (timesTriggered >= maxTriggerCount) { return false; } return abilityConditions.All(c => c.MatchesCondition(abilityData)); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs index 6597bbcd6..4e3d0896f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs @@ -14,6 +14,7 @@ namespace Barotrauma.Abilities private float effectDelay; private float effectDelayTimer; + public CharacterAbilityGroupInterval(CharacterTalent characterTalent, XElement abilityElementGroup) : base(characterTalent, abilityElementGroup) { // too many overlapping intervals could cause hitching? maybe randomize a little @@ -42,6 +43,7 @@ namespace Barotrauma.Abilities } private bool IsApplicable() { + if (timesTriggered >= maxTriggerCount) { return false; } return abilityConditions.All(c => c.MatchesCondition()); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs index 9c64f85ea..e9ac4347a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs @@ -111,8 +111,7 @@ namespace Barotrauma public static AbilityEffectType ParseAbilityEffectType(CharacterTalent characterTalent, string abilityEffectTypeString) { - AbilityEffectType abilityEffectType = AbilityEffectType.Undefined; - if (!Enum.TryParse(abilityEffectTypeString, true, out abilityEffectType)) + if (!Enum.TryParse(abilityEffectTypeString, true, out AbilityEffectType abilityEffectType)) { DebugConsole.ThrowError("Invalid ability effect type \"" + abilityEffectTypeString + "\" in CharacterTalent (" + characterTalent.DebugIdentifier + ")"); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs index 5c42eb8e7..277af65b0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs @@ -1,7 +1,5 @@ -using Microsoft.Xna.Framework; -using System; +using System; using System.Collections.Generic; -using System.Linq; using System.Xml.Linq; namespace Barotrauma @@ -13,6 +11,12 @@ namespace Barotrauma public ContentPackage ContentPackage { get; private set; } public string FilePath { get; private set; } + public string DisplayName { get; private set; } + + public string Description { get; private set; } + + public readonly Sprite Icon; + public static readonly PrefabCollection TalentPrefabs = new PrefabCollection(); public XElement ConfigElement @@ -26,7 +30,51 @@ namespace Barotrauma FilePath = filePath; ConfigElement = element; Identifier = element.GetAttributeString("identifier", "noidentifier"); + DisplayName = TextManager.Get("talentname." + Identifier, returnNull: true) ?? Identifier; this.CalculatePrefabUIntIdentifier(TalentPrefabs); + + foreach (XElement subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "icon": + Icon = new Sprite(subElement); + break; + case "description": + string tempDescription = Description; + TextManager.ConstructDescription(ref tempDescription, subElement); + Description = tempDescription; + break; + } + } + + if (string.IsNullOrEmpty(Description)) + { + if (element.Attribute("description") != null) + { + string description = element.GetAttributeString("description", string.Empty); + Description = TextManager.Get(description, returnNull: true) ?? description; + } + else + { + Description = TextManager.Get("talentdescription." + Identifier, returnNull: true) ?? string.Empty; + } + } + +#if DEBUG + if (!TextManager.ContainsTag("talentname." + Identifier)) + { + DebugConsole.AddWarning($"Name for the talent \"{Identifier}\" not found in the text files."); + } + if (string.IsNullOrEmpty(Description)) + { + DebugConsole.AddWarning($"Description for the talent \"{Identifier}\" not configured"); + } + if (Description.Contains('[')) + { + DebugConsole.ThrowError($"Description for the talent \"{Identifier}\" contains brackets - was some variable not replaced correctly? ({Description})"); + } +#endif } private bool disposed = false; @@ -94,9 +142,6 @@ namespace Barotrauma { LoadFromFile(file); } - } - - } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs index 2a2f67bdf..6483edaa1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs @@ -14,6 +14,7 @@ namespace Barotrauma private static HashSet subtreeTalents = new HashSet(); + private const string PlaceholderTalent = "placeholder"; public XElement ConfigElement { get; @@ -41,6 +42,7 @@ namespace Barotrauma HashSet duplicateSet = new HashSet(); foreach (string talent in TalentSubTrees.SelectMany(s => s.TalentOptionStages.SelectMany(o => o.Talents.Select(t => t.Identifier)))) { + if (talent == PlaceholderTalent) { continue; } TalentPrefab talentPrefab = TalentPrefab.TalentPrefabs.Find(c => c.Identifier.Equals(talent, StringComparison.OrdinalIgnoreCase)); if (talentPrefab == null) { @@ -118,6 +120,7 @@ namespace Barotrauma public static bool IsViableTalentForCharacter(Character character, string talentIdentifier, IEnumerable selectedTalents) { + if (talentIdentifier == PlaceholderTalent) { return false; } if (character?.Info?.Job.Prefab == null) { return false; } if (character.Info.GetTotalTalentPoints() - selectedTalents.Count() <= 0) { return false; } @@ -178,7 +181,7 @@ namespace Barotrauma foreach (XElement talentOptionsElement in subTreeElement.GetChildElements("talentoptions")) { - TalentOptionStages.Add(new TalentOption(talentOptionsElement)); + TalentOptionStages.Add(new TalentOption(talentOptionsElement, Identifier)); } } @@ -186,32 +189,19 @@ namespace Barotrauma class TalentOption { - public readonly List Talents = new List(); + public readonly List Talents = new List(); - public TalentOption(XElement talentOptionsElement) + public TalentOption(XElement talentOptionsElement, string debugIdentifier) { foreach (XElement talentOptionElement in talentOptionsElement.GetChildElements("talentoption")) { - Talents.Add(new Talent(talentOptionElement)); - } - } - } - - class Talent - { - public readonly string Identifier; - public readonly Sprite Icon; - public Talent(XElement talentOptionElement) - { - Identifier = talentOptionElement.GetAttributeString("identifier", ""); - foreach (XElement subElement in talentOptionElement.Elements()) - { - switch (subElement.Name.ToString().ToLowerInvariant()) + string identifier = talentOptionElement.GetAttributeString("identifier", string.Empty); + if (!TalentPrefab.TalentPrefabs.ContainsKey(identifier)) { - case "icon": - Icon = new Sprite(subElement); - break; + DebugConsole.ThrowError($"Error in talent tree \"{debugIdentifier}\" - could not find a talent with the identifier \"{identifier}\"."); + return; } + Talents.Add(TalentPrefab.TalentPrefabs[identifier]); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index e768d963d..39810a12f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -212,7 +212,7 @@ namespace Barotrauma }; }, isCheat: true)); - commands.Add(new Command("spawnitem", "spawnitem [itemname/itemidentifier] [cursor/inventory/cargo/random/[name]]: Spawn an item at the position of the cursor, in the inventory of the controlled character, in the inventory of the client with the given name, or at a random spawnpoint if the last parameter is omitted or \"random\".", + commands.Add(new Command("spawnitem", "spawnitem [itemname/itemidentifier] [cursor/inventory/cargo/random/[name]] [amount]: Spawn an item at the position of the cursor, in the inventory of the controlled character, in the inventory of the client with the given name, or at a random spawnpoint if the last parameter is omitted or \"random\".", (string[] args) => { try @@ -841,8 +841,8 @@ namespace Barotrauma commands.Add(new Command("givetalent", "give [player] testing [talent]", (string[] args) => { - if (args.Length < 2) return; - var character = FindMatchingCharacter(args.Skip(1).ToArray()) ?? Character.Controlled; + if (args.Length == 0) { return; } + var character = args.Length >= 2 ? FindMatchingCharacter(args.Skip(1).ToArray()) : Character.Controlled; if (character != null) { character.GiveTalent(args[0]); @@ -858,8 +858,8 @@ namespace Barotrauma return new string[][] { - talentNames.ToArray(), - Character.CharacterList.Select(c => c.Name).Distinct().ToArray() + talentNames.ToArray(), + Character.CharacterList.Select(c => c.Name).Distinct().ToArray() }; }, isCheat: true)); @@ -2033,9 +2033,17 @@ namespace Barotrauma return; } + int amount = 1; if (args.Length > 1) { - switch (args.Last()) + string spawnLocation = args.Last(); + if (args.Length > 2) + { + spawnLocation = args[^2]; + if (!int.TryParse(args[^1], NumberStyles.Any, CultureInfo.InvariantCulture, out amount)) { amount = 1; } + } + + switch (spawnLocation) { case "cursor": spawnPos = cursorPos; @@ -2063,37 +2071,40 @@ namespace Barotrauma spawnPos = wp == null ? Vector2.Zero : wp.WorldPosition; } - if (spawnPos != null) + for (int i = 0; i < amount; i++) { - if (Entity.Spawner == null) + if (spawnPos != null) { - new Item(itemPrefab, spawnPos.Value, null); - } - else - { - Entity.Spawner?.AddToSpawnQueue(itemPrefab, spawnPos.Value); - } - } - else if (spawnInventory != null) - { - if (Entity.Spawner == null) - { - var spawnedItem = new Item(itemPrefab, Vector2.Zero, null); - spawnInventory.TryPutItem(spawnedItem, null, spawnedItem.AllowedSlots); - onItemSpawned(spawnedItem); - } - else - { - Entity.Spawner?.AddToSpawnQueue(itemPrefab, spawnInventory, onSpawned: onItemSpawned); - } - - static void onItemSpawned(Item item) - { - if (item.ParentInventory?.Owner is Character character) + if (Entity.Spawner == null) { - foreach (WifiComponent wifiComponent in item.GetComponents()) + new Item(itemPrefab, spawnPos.Value, null); + } + else + { + Entity.Spawner?.AddToSpawnQueue(itemPrefab, spawnPos.Value); + } + } + else if (spawnInventory != null) + { + if (Entity.Spawner == null) + { + var spawnedItem = new Item(itemPrefab, Vector2.Zero, null); + spawnInventory.TryPutItem(spawnedItem, null, spawnedItem.AllowedSlots); + onItemSpawned(spawnedItem); + } + else + { + Entity.Spawner?.AddToSpawnQueue(itemPrefab, spawnInventory, onSpawned: onItemSpawned); + } + + static void onItemSpawned(Item item) + { + if (item.ParentInventory?.Owner is Character character) { - wifiComponent.TeamID = character.TeamID; + foreach (WifiComponent wifiComponent in item.GetComponents()) + { + wifiComponent.TeamID = character.TeamID; + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs index c127555da..346ceb31e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs @@ -45,14 +45,18 @@ OnReduceAffliction, OnAddDamageAffliction, OnSelfRagdoll, + OnRoundEnd, OnAnyMissionCompleted, OnAllMissionsCompleted, OnGiveOrder, OnCrewKillCharacter, + OnKillCharacter, OnDieToCharacter, OnAllyGainMissionExperience, OnGainMissionExperience, OnGainMissionMoney, + OnItemDeconstructed, + OnItemDeconstructedMaterial, AfterSubmarineAttacked, } @@ -68,23 +72,32 @@ // Character attributes MaximumHealthMultiplier, MovementSpeed, + WalkingSpeed, SwimmingSpeed, BuffDurationMultiplier, DebuffDurationMultiplier, + MedicalItemEffectivenessMultiplier, // Combat AttackMultiplier, + TeamAttackMultiplier, RangedAttackSpeed, TurretAttackSpeed, + TurretPowerCostReduction, MeleeAttackSpeed, - SpreadMultiplier, + MeleeAttackMultiplier, + RangedSpreadReduction, // Utility RepairSpeed, + DeconstructorSpeedMultiplier, // Misc ReputationGainMultiplier, MissionMoneyGainMultiplier, ExperienceGainMultiplier, MissionExperienceGainMultiplier, - + // these should be deprecated and moved to their own implementation, no sense making them share space with stat values + Coathor, + WarriorPoetMissionRuns, + WarriorPoetEnemiesKilled, } public enum AbilityFlags @@ -95,6 +108,8 @@ IgnoredByEnemyAI, MoveNormallyWhileDragging, CanTinker, + GainSkillPastMaximum, + RetainExperienceForNewCharacter } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index 47b9637e2..d6adbf7b5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -362,11 +362,16 @@ namespace Barotrauma } // apply money gains afterwards to prevent them from affecting XP gains - var moneyGainMultiplier = new AbilityValue(1f); - crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnGainMissionMoney, (this, moneyGainMultiplier))); - crewCharacters.ForEach(c => moneyGainMultiplier.Value += c.GetStatValue(StatTypes.MissionMoneyGainMultiplier)); + var moneyGainMission = new AbilityValueMission(1f, this); + crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnGainMissionMoney, moneyGainMission)); + crewCharacters.ForEach(c => moneyGainMission.Value += c.GetStatValue(StatTypes.MissionMoneyGainMultiplier)); - campaign.Money += (int)(reward * moneyGainMultiplier.Value); + campaign.Money += (int)(reward * moneyGainMission.Value); + + foreach (Character character in crewCharacters) + { + character.Info.MissionsCompletedSinceDeath++; + } foreach (KeyValuePair reputationReward in ReputationRewards) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index fdf7fa68a..4f941e847 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -531,6 +531,7 @@ namespace Barotrauma if (Level.Loaded.EndOutpost == null) { Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndExitPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID); + if (closestSub == null) { return null; } return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub; } else diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs index b5bb43eca..739f6f112 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -173,6 +173,12 @@ namespace Barotrauma if (matchingSub != null) { availableSubs.Add(matchingSub); } } break; + case "savedexperiencepoints": + foreach (XElement savedExp in subElement.Elements()) + { + savedExperiencePoints.Add(new SavedExperiencePoints(savedExp)); + } + break; #endif } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index 0a2e0bdd3..8b6cbeb36 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -661,7 +661,7 @@ namespace Barotrauma #if SERVER return GameMain.Server.ConnectedClients.Select(c => c.Character).Where(c => c.Info != null); #else - return GameMain.GameSession.CrewManager.CharacterInfos.Select(i => i.Character).Where(c => c != null); + return GameMain.GameSession.CrewManager.GetCharacters().Where(c => c.Info != null); #endif } @@ -671,28 +671,33 @@ namespace Barotrauma try { - IEnumerable crewCharacters = GameSession.GetSessionCrewCharacters(); + IEnumerable crewCharacters = GetSessionCrewCharacters(); foreach (Mission mission in missions) { mission.End(); } + foreach (Character character in crewCharacters) + { + character.CheckTalents(AbilityEffectType.OnRoundEnd); + } + if (missions.Any()) { if (missions.Any(m => m.Completed)) { - foreach (CharacterInfo characterInfo in GameMain.GameSession.CrewManager.CharacterInfos) + foreach (Character character in crewCharacters) { - characterInfo.Character?.CheckTalents(AbilityEffectType.OnAnyMissionCompleted); + character.CheckTalents(AbilityEffectType.OnAnyMissionCompleted); } } if (missions.All(m => m.Completed)) { - foreach (CharacterInfo characterInfo in GameMain.GameSession.CrewManager.CharacterInfos) + foreach (Character character in crewCharacters) { - characterInfo.Character?.CheckTalents(AbilityEffectType.OnAllMissionsCompleted); + character.CheckTalents(AbilityEffectType.OnAllMissionsCompleted); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs index 49bfdb3e8..8b65d6f30 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs @@ -9,7 +9,7 @@ namespace Barotrauma [Flags] public enum InvSlotType { - None = 0, Any = 1, RightHand = 2, LeftHand = 4, Head = 8, InnerClothes = 16, OuterClothes = 32, Headset = 64, Card = 128, Bag = 256 + None = 0, Any = 1, RightHand = 2, LeftHand = 4, Head = 8, InnerClothes = 16, OuterClothes = 32, Headset = 64, Card = 128, Bag = 256, HealthInterface = 512 }; partial class CharacterInventory : Inventory diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs new file mode 100644 index 000000000..e9aa55e2b --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.Generic; +using System.Xml.Linq; +using System.Linq; +using Barotrauma.Extensions; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; + +namespace Barotrauma.Items.Components +{ + partial class GeneticMaterial : ItemComponent, IServerSerializable + { + private readonly string materialName; + + private Character targetCharacter; + private AfflictionPrefab selectedEffect, selectedTaintedEffect; + + [Serialize("", false)] + public string Effect + { + get; + set; + } + + [Serialize("geneticmaterialdebuff", false)] + public string TaintedEffect + { + get; + set; + } + + private bool tainted; + [Serialize(false, false)] + public bool Tainted + { + get { return tainted; } + private set + { + if (!value) { return; } + tainted = true; + item.AllowDeconstruct = false; + if (!string.IsNullOrEmpty(TaintedEffect)) + { + selectedTaintedEffect = AfflictionPrefab.Prefabs.Where(a => + a.Identifier.Equals(TaintedEffect, StringComparison.OrdinalIgnoreCase) || + a.AfflictionType.Equals(TaintedEffect, StringComparison.OrdinalIgnoreCase)).GetRandom(); + } + } + } + + //only for saving the selected tainted effect + [Serialize("", false)] + public string SelectedTaintedEffect + { + get { return selectedTaintedEffect?.Identifier ?? string.Empty; } + private set + { + if (string.IsNullOrEmpty(value)) { return; } + selectedTaintedEffect = AfflictionPrefab.Prefabs.Find(a => a.Identifier == value); + } + } + + + public GeneticMaterial(Item item, XElement element) + : base(item, element) + { + string nameId = element.GetAttributeString("nameidentifier", ""); + if (!string.IsNullOrEmpty(nameId)) + { + materialName = TextManager.Get(nameId); + } + if (!string.IsNullOrEmpty(Effect)) + { + selectedEffect = AfflictionPrefab.Prefabs.Where(a => + a.Identifier.Equals(Effect, StringComparison.OrdinalIgnoreCase) || + a.AfflictionType.Equals(Effect, StringComparison.OrdinalIgnoreCase)).GetRandom(); + } + } + + [Serialize(3.0f, false)] + public float ConditionIncreaseOnCombineMin { get; set; } + + [Serialize(8.0f, false)] + public float ConditionIncreaseOnCombineMax { get; set; } + + public bool CanBeCombinedWith(GeneticMaterial otherGeneticMaterial) + { + return !tainted && otherGeneticMaterial != null && !otherGeneticMaterial.tainted; + } + + public override void Equip(Character character) + { + if (character == null) { return; } + IsActive = true; + + if (targetCharacter != null) { return; } + + if (tainted) + { + if (selectedTaintedEffect != null) + { + float selectedTaintedEffectStrength = item.ConditionPercentage / 100.0f * selectedTaintedEffect.MaxStrength; + character.CharacterHealth.ApplyAffliction(null, selectedTaintedEffect.Instantiate(selectedTaintedEffectStrength)); + targetCharacter = character; +#if SERVER + item.CreateServerEvent(this); +#endif + } + } + if (selectedEffect != null) + { + ApplyStatusEffects(ActionType.OnWearing, 1.0f); + float selectedEffectStrength = item.ConditionPercentage / 100.0f * selectedEffect.MaxStrength; + character.CharacterHealth.ApplyAffliction(null, selectedEffect.Instantiate(selectedEffectStrength)); + targetCharacter = character; +#if SERVER + item.CreateServerEvent(this); +#endif + } + } + + public override void Update(float deltaTime, Camera cam) + { + base.Update(deltaTime, cam); + if (targetCharacter != null) + { + if (!targetCharacter.HasEquippedItem(item) && + (item.Container == null || !targetCharacter.HasEquippedItem(item.Container) || !(item.Container.GetComponent()?.AutoInject ?? false))) + { + item.ApplyStatusEffects(ActionType.OnSevered, 1.0f, targetCharacter); + var currentEffect = tainted ? selectedTaintedEffect : selectedEffect; + targetCharacter.CharacterHealth.ReduceAffliction(null, currentEffect.Identifier, currentEffect.MaxStrength); + targetCharacter = null; + IsActive = false; + } + } + } + + public bool Combine(GeneticMaterial otherGeneticMaterial, Character user) + { + if (!CanBeCombinedWith(otherGeneticMaterial)) { return false; } + if (item.Prefab == otherGeneticMaterial.item.Prefab) + { + item.Condition = Math.Max(item.Condition, otherGeneticMaterial.item.Condition) + Rand.Range(ConditionIncreaseOnCombineMin, ConditionIncreaseOnCombineMax); + float taintedProbability = GetTaintedProbabilityOnRefine(user); + if (taintedProbability >= Rand.Range(0.0f, 1.0f)) + { + MakeTainted(); + } + return true; + } + else + { + item.Condition = otherGeneticMaterial.Item.Condition = + (item.Condition + otherGeneticMaterial.Item.Condition) / 2.0f + Rand.Range(ConditionIncreaseOnCombineMin, ConditionIncreaseOnCombineMax); + item.OwnInventory?.TryPutItem(otherGeneticMaterial.Item, user: null); + MakeTainted(); + return false; + } + } + + private float GetTaintedProbabilityOnRefine(Character user) + { + if (user == null) { return 1.0f; } + float probability = MathHelper.Lerp(0.0f, 0.99f, item.Condition / 100.0f); + probability *= MathHelper.Lerp(1.0f, 0.25f, DegreeOfSuccess(user)); + return probability; + } + + private void MakeTainted() + { + if (GameMain.NetworkMember?.IsClient ?? false) { return; } + Tainted = true; +#if SERVER + item.CreateServerEvent(this); +#endif + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index b7174abdf..312df8e44 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -181,7 +181,7 @@ namespace Barotrauma.Items.Components if (aim) { hitPos = MathUtils.WrapAnglePi(Math.Min(hitPos + deltaTime * 5f, MathHelper.PiOver4)); - ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, false, hitPos, holdAngle + hitPos); + ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, false, hitPos, holdAngle + hitPos, aimingMelee: true); } else { @@ -356,6 +356,7 @@ namespace Barotrauma.Items.Components if (Attack != null) { Attack.SetUser(User); + Attack.DamageMultiplier = 1 + User.GetStatValue(StatTypes.MeleeAttackMultiplier); if (targetLimb != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs index d628c5d40..14d626d16 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs @@ -107,7 +107,7 @@ namespace Barotrauma.Items.Components if (ReloadTimer < 0.0f) { ReloadTimer = 0.0f; - // was this an optimization or related to something else? currently disabled for charge-type weapons + // was this an optimization or related to something else? it cannot occur for charge-type weapons //IsActive = false; if (MaxChargeTime == 0.0f) { @@ -118,7 +118,7 @@ namespace Barotrauma.Items.Components float previousChargeTime = currentChargeTime; - float chargeDeltaTime = tryingToCharge ? deltaTime : -deltaTime; + float chargeDeltaTime = tryingToCharge && ReloadTimer <= 0f ? deltaTime : -deltaTime; currentChargeTime = Math.Clamp(currentChargeTime + chargeDeltaTime, 0f, MaxChargeTime); tryingToCharge = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index 90fd92739..c146ae67e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -982,7 +982,7 @@ namespace Barotrauma.Items.Components AIObjectiveContainItem containObjective = null; if (character.AIController is HumanAIController aiController) { - containObjective = new AIObjectiveContainItem(character, container.GetContainableItemIdentifiers.ToArray(), container, currentObjective.objectiveManager, spawnItemIfNotFound: spawnItemIfNotFound) + containObjective = new AIObjectiveContainItem(character, container.ContainableItemIdentifiers.ToArray(), container, currentObjective.objectiveManager, spawnItemIfNotFound: spawnItemIfNotFound) { targetItemCount = itemCount, Equip = equip, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index 336a5b183..76093809d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Xml.Linq; using Barotrauma.Extensions; using FarseerPhysics; +using System.Collections.Immutable; namespace Barotrauma.Items.Components { @@ -23,6 +24,28 @@ namespace Barotrauma.Items.Components } } + class SlotRestrictions + { + public readonly int MaxStackSize; + public readonly List ContainableItems; + + public SlotRestrictions(int maxStackSize, List containableItems) + { + MaxStackSize = maxStackSize; + ContainableItems = containableItems; + } + + public bool MatchesItem(Item item) + { + return ContainableItems == null || ContainableItems.Count == 0 || ContainableItems.Any(c => c.MatchesItem(item)); + } + + public bool MatchesItem(ItemPrefab itemPrefab) + { + return ContainableItems == null || ContainableItems.Count == 0 || ContainableItems.Any(c => c.MatchesItem(itemPrefab)); + } + } + private bool alwaysContainedItemsSpawned; public ItemInventory Inventory; @@ -73,6 +96,7 @@ namespace Barotrauma.Items.Components #endif [Serialize("0.0,0.0", false, description: "The interval at which the contained items are spaced apart from each other (in pixels).")] public Vector2 ItemInterval { get; set; } + [Serialize(100, false, description: "How many items are placed in a row before starting a new row.")] public int ItemsPerRow { get; set; } @@ -90,7 +114,6 @@ namespace Barotrauma.Items.Components set; } - [Serialize(false, false, description: "If set to true, interacting with this item will make the character interact with the contained item(s), automatically picking them up if they can be picked up.")] public bool AutoInteractWithContained { @@ -98,6 +121,9 @@ namespace Barotrauma.Items.Components set; } + [Serialize(true, false)] + public bool AllowAccess { get; set; } + [Serialize(false, false)] public bool AccessOnlyWhenBroken { get; set; } @@ -147,7 +173,7 @@ namespace Barotrauma.Items.Components set; } - [Serialize(0.5f, false, description: "The rotation in which the contained sprites are drawn (in degrees).")] + [Serialize(0.5f, false, description: "The health threshold that the user must reach in order to activate the autoinjection.")] public float AutoInjectThreshold { get; @@ -157,10 +183,12 @@ namespace Barotrauma.Items.Components [Serialize(false, false)] public bool RemoveContainedItemsOnDeconstruct { get; set; } + private SlotRestrictions[] slotRestrictions; + public bool ShouldBeContained(string[] identifiersOrTags, out bool isRestrictionsDefined) { isRestrictionsDefined = containableRestrictions.Any(); - if (ContainableItems.None(ri => ri.MatchesItem(item))) { return false; } + if (slotRestrictions.None(s => s.MatchesItem(item))) { return false; } if (!isRestrictionsDefined) { return true; } return identifiersOrTags.Any(id => containableRestrictions.Any(r => r == id)); } @@ -168,22 +196,22 @@ namespace Barotrauma.Items.Components public bool ShouldBeContained(Item item, out bool isRestrictionsDefined) { isRestrictionsDefined = containableRestrictions.Any(); - if (ContainableItems.None(ri => ri.MatchesItem(item))) { return false; } + if (slotRestrictions.None(s => s.MatchesItem(item))) { return false; } if (!isRestrictionsDefined) { return true; } return containableRestrictions.Any(id => item.Prefab.Identifier == id || item.HasTag(id)); } - public List ContainableItems { get; private set; } = new List(); - - public IEnumerable GetContainableItemIdentifiers => ContainableItems.SelectMany(ri => ri.Identifiers); + private ImmutableHashSet containableItemIdentifiers; + public IEnumerable ContainableItemIdentifiers => containableItemIdentifiers; public override bool RecreateGUIOnResolutionChange => true; public ItemContainer(Item item, XElement element) - : base (item, element) + : base(item, element) { - Inventory = new ItemInventory(item, this, capacity, SlotsPerRow); - + int totalCapacity = capacity; + + List containableItems = null; foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -195,34 +223,92 @@ namespace Barotrauma.Items.Components DebugConsole.ThrowError("Error in item config \"" + item.ConfigFile + "\" - containable with no identifiers."); continue; } - ContainableItems.Add(containable); + containableItems ??= new List(); + containableItems.Add(containable); + break; + case "subcontainer": + totalCapacity += subElement.GetAttributeInt("capacity", 1); break; } } + Inventory = new ItemInventory(item, this, totalCapacity, SlotsPerRow); + slotRestrictions = new SlotRestrictions[totalCapacity]; + for (int i = 0; i < capacity; i++) + { + slotRestrictions[i] = new SlotRestrictions(maxStackSize, containableItems); + } + int subContainerIndex = capacity; + foreach (XElement subElement in element.Elements()) + { + if (subElement.Name.ToString().ToLowerInvariant() != "subcontainer") { continue; } + + int subCapacity = subElement.GetAttributeInt("capacity", 1); + int subMaxStackSize = subElement.GetAttributeInt("maxstacksize", maxStackSize); + + List subContainableItems = null; + foreach (XElement subSubElement in subElement.Elements()) + { + if (subSubElement.Name.ToString().ToLowerInvariant() != "containable") { continue; } + + RelatedItem containable = RelatedItem.Load(subSubElement, returnEmpty: false, parentDebugName: item.Name); + if (containable == null) + { + DebugConsole.ThrowError("Error in item config \"" + item.ConfigFile + "\" - containable with no identifiers."); + continue; + } + subContainableItems ??= new List(); + subContainableItems.Add(containable); + } + + for (int i = subContainerIndex; i < subContainerIndex + subCapacity; i++) + { + slotRestrictions[i] = new SlotRestrictions(subMaxStackSize, subContainableItems); + } + subContainerIndex += subCapacity; + } + capacity = totalCapacity; InitProjSpecific(element); } + public int GetMaxStackSize(int slotIndex) + { + if (slotIndex < 0 || slotIndex >= capacity) + { + return 0; + } + return slotRestrictions[slotIndex].MaxStackSize; + } + partial void InitProjSpecific(XElement element); public void OnItemContained(Item containedItem) { item.SetContainedItemPositions(); - - RelatedItem ri = ContainableItems.Find(x => x.MatchesItem(containedItem)); - if (ri != null) + + int index = Inventory.FindIndex(containedItem); + if (index >= 0 && index < slotRestrictions.Length) { - activeContainedItems.RemoveAll(i => i.Item == containedItem); - foreach (StatusEffect effect in ri.statusEffects) + RelatedItem ri = slotRestrictions[index].ContainableItems?.Find(ci => ci.MatchesItem(containedItem)); + if (ri != null) { - activeContainedItems.Add(new ActiveContainedItem(containedItem, effect, ri.ExcludeBroken)); + activeContainedItems.RemoveAll(i => i.Item == containedItem); + foreach (StatusEffect effect in ri.statusEffects) + { + activeContainedItems.Add(new ActiveContainedItem(containedItem, effect, ri.ExcludeBroken)); + } } - } + } //no need to Update() if this item has no statuseffects and no physics body IsActive = activeContainedItems.Count > 0 || Inventory.AllItems.Any(it => it.body != null); } + public override void Move(Vector2 amount) + { + SetContainedItemPositions(); + } + public void OnItemRemoved(Item containedItem) { activeContainedItems.RemoveAll(i => i.Item == containedItem); @@ -233,13 +319,11 @@ namespace Barotrauma.Items.Components public bool CanBeContained(Item item) { - if (ContainableItems.Count == 0) { return true; } - return ContainableItems.Find(c => c.MatchesItem(item)) != null; + return slotRestrictions.Any(s => s.MatchesItem(item)); } public bool CanBeContained(ItemPrefab itemPrefab) { - if (ContainableItems.Count == 0) { return true; } - return ContainableItems.Find(c => c.MatchesItem(itemPrefab)) != null; + return slotRestrictions.Any(s => s.MatchesItem(itemPrefab)); } readonly List targets = new List(); @@ -264,6 +348,7 @@ namespace Barotrauma.Items.Components foreach (Item item in Inventory.AllItemsMod) { item.ApplyStatusEffects(ActionType.OnUse, 1.0f, ownerCharacter); + item.GetComponent()?.Equip(ownerCharacter); } } } @@ -304,11 +389,12 @@ namespace Barotrauma.Items.Components public override bool HasRequiredItems(Character character, bool addMessage, string msg = null) { - return (!AccessOnlyWhenBroken || Item.Condition <= 0) && base.HasRequiredItems(character, addMessage, msg); + return AllowAccess && (!AccessOnlyWhenBroken || Item.Condition <= 0) && base.HasRequiredItems(character, addMessage, msg); } public override bool Select(Character character) { + if (!AllowAccess) { return false; } if (item.Container != null) { return false; } if (AccessOnlyWhenBroken) { @@ -335,6 +421,7 @@ namespace Barotrauma.Items.Components public override bool Pick(Character picker) { + if (!AllowAccess) { return false; } if (AccessOnlyWhenBroken) { if (item.Condition > 0) @@ -362,7 +449,7 @@ namespace Barotrauma.Items.Components public override bool Combine(Item item, Character user) { if (!AllowDragAndDrop && user != null) { return false; } - if (!ContainableItems.Any(it => it.MatchesItem(item))) { return false; } + if (!slotRestrictions.Any(s => s.MatchesItem(item))) { return false; } if (user != null && !user.CanAccessInventory(Inventory)) { return false; } if (Inventory.TryPutItem(item, user)) @@ -392,50 +479,59 @@ namespace Barotrauma.Items.Components Vector2 transformedItemInterval = ItemInterval * item.Scale; Vector2 transformedItemIntervalHorizontal = new Vector2(transformedItemInterval.X, 0.0f); Vector2 transformedItemIntervalVertical = new Vector2(0.0f, transformedItemInterval.Y); - if (item.body == null) + + if (ItemPos == Vector2.Zero && ItemInterval == Vector2.Zero) { - if (item.FlippedX) - { - transformedItemPos.X = -transformedItemPos.X; - transformedItemPos.X += item.Rect.Width; - transformedItemInterval.X = -transformedItemInterval.X; - transformedItemIntervalHorizontal.X = -transformedItemIntervalHorizontal.X; - } - if (item.FlippedY) - { - transformedItemPos.Y = -transformedItemPos.Y; - transformedItemPos.Y -= item.Rect.Height; - transformedItemInterval.Y = -transformedItemInterval.Y; - transformedItemIntervalVertical.Y = -transformedItemIntervalVertical.Y; - } - transformedItemPos += new Vector2(item.Rect.X, item.Rect.Y); - if (Math.Abs(item.Rotation) > 0.01f) - { - Matrix transform = Matrix.CreateRotationZ(MathHelper.ToRadians(-item.Rotation)); - transformedItemPos = Vector2.Transform(transformedItemPos, transform); - transformedItemInterval = Vector2.Transform(transformedItemInterval, transform); - transformedItemIntervalHorizontal = Vector2.Transform(transformedItemIntervalHorizontal, transform); - transformedItemIntervalVertical = Vector2.Transform(transformedItemIntervalVertical, transform); - } + transformedItemPos = item.Position; } else { - Matrix transform = Matrix.CreateRotationZ(item.body.Rotation); - if (item.body.Dir == -1.0f) + if (item.body == null) { - transformedItemPos.X = -transformedItemPos.X; - transformedItemInterval.X = -transformedItemInterval.X; - transformedItemIntervalHorizontal.X = -transformedItemIntervalHorizontal.X; + if (item.FlippedX) + { + transformedItemPos.X = -transformedItemPos.X; + transformedItemPos.X += item.Rect.Width; + transformedItemInterval.X = -transformedItemInterval.X; + transformedItemIntervalHorizontal.X = -transformedItemIntervalHorizontal.X; + } + if (item.FlippedY) + { + transformedItemPos.Y = -transformedItemPos.Y; + transformedItemPos.Y -= item.Rect.Height; + transformedItemInterval.Y = -transformedItemInterval.Y; + transformedItemIntervalVertical.Y = -transformedItemIntervalVertical.Y; + } + transformedItemPos += new Vector2(item.Rect.X, item.Rect.Y); + if (Math.Abs(item.Rotation) > 0.01f) + { + Matrix transform = Matrix.CreateRotationZ(MathHelper.ToRadians(-item.Rotation)); + transformedItemPos = Vector2.Transform(transformedItemPos, transform); + transformedItemInterval = Vector2.Transform(transformedItemInterval, transform); + transformedItemIntervalHorizontal = Vector2.Transform(transformedItemIntervalHorizontal, transform); + transformedItemIntervalVertical = Vector2.Transform(transformedItemIntervalVertical, transform); + } } - transformedItemPos = Vector2.Transform(transformedItemPos, transform); - transformedItemInterval = Vector2.Transform(transformedItemInterval, transform); - transformedItemIntervalHorizontal = Vector2.Transform(transformedItemIntervalHorizontal, transform); - transformedItemPos += item.Position; - } + else + { + Matrix transform = Matrix.CreateRotationZ(item.body.Rotation); + if (item.body.Dir == -1.0f) + { + transformedItemPos.X = -transformedItemPos.X; + transformedItemInterval.X = -transformedItemInterval.X; + transformedItemIntervalHorizontal.X = -transformedItemIntervalHorizontal.X; + } + transformedItemPos = Vector2.Transform(transformedItemPos, transform); + transformedItemInterval = Vector2.Transform(transformedItemInterval, transform); + transformedItemIntervalHorizontal = Vector2.Transform(transformedItemIntervalHorizontal, transform); + transformedItemPos += item.Position; + } + } float currentRotation = itemRotation; if (item.body != null) { + currentRotation *= item.body.Dir; currentRotation += item.body.Rotation; } @@ -492,6 +588,7 @@ namespace Barotrauma.Items.Components public override void OnItemLoaded() { + containableItemIdentifiers = slotRestrictions.SelectMany(s => s.ContainableItems?.SelectMany(ri => ri.Identifiers) ?? Enumerable.Empty()).ToImmutableHashSet(); if (item.Submarine == null || !item.Submarine.Loading) { SpawnAlwaysContainedItems(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs index 575401584..3fe8d3b74 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs @@ -1,4 +1,5 @@ -using Barotrauma.Extensions; +using Barotrauma.Abilities; +using Barotrauma.Extensions; using Barotrauma.Networking; using System; using System.Collections.Generic; @@ -14,6 +15,10 @@ namespace Barotrauma.Items.Components private bool hasPower; + private Character user; + + private float userDeconstructorSpeedMultiplier = 1.0f; + private ItemContainer inputContainer, outputContainer; public ItemContainer InputContainer @@ -25,7 +30,10 @@ namespace Barotrauma.Items.Components { get { return outputContainer; } } - + + [Serialize(false, true)] + public bool DeconstructItemsSimultaneously { get; set; } + [Editable, Serialize(1.0f, true)] public float DeconstructionSpeed { get; set; } @@ -81,65 +89,149 @@ namespace Barotrauma.Items.Components if (powerConsumption <= 0.0f) { Voltage = 1.0f; } progressTimer += deltaTime * Math.Min(Voltage, 1.0f); - var targetItem = inputContainer.Inventory.LastOrDefault(); - if (targetItem == null) { return; } - - float deconstructTime = targetItem.Prefab.DeconstructItems.Any() ? targetItem.Prefab.DeconstructTime / DeconstructionSpeed : 1.0f; - - progressState = Math.Min(progressTimer / deconstructTime, 1.0f); - if (progressTimer > deconstructTime) + if (DeconstructItemsSimultaneously) { - // In multiplayer, the server handles the deconstruction into new items - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } - - if (targetItem.Prefab.RandomDeconstructionOutput) + float deconstructTime = 0.0f; + foreach (Item targetItem in inputContainer.Inventory.AllItems) { - int amount = targetItem.Prefab.RandomDeconstructionOutputAmount; - List deconstructItemIndexes = new List(); - for (int i = 0; i < targetItem.Prefab.DeconstructItems.Count; i++) - { - deconstructItemIndexes.Add(i); - } - List commonness = targetItem.Prefab.DeconstructItems.Select(i => i.Commonness).ToList(); - List products = new List(); - - for (int i = 0; i < amount; i++) - { - if (deconstructItemIndexes.Count < 1) { break; } - var itemIndex = ToolBox.SelectWeightedRandom(deconstructItemIndexes, commonness, Rand.RandSync.Unsynced); - products.Add(targetItem.Prefab.DeconstructItems[itemIndex]); - var removeIndex = deconstructItemIndexes.IndexOf(itemIndex); - deconstructItemIndexes.RemoveAt(removeIndex); - commonness.RemoveAt(removeIndex); - } - foreach (DeconstructItem deconstructProduct in products) - { - CreateDeconstructProduct(deconstructProduct); - } - } - else - { - foreach (DeconstructItem deconstructProduct in targetItem.Prefab.DeconstructItems) - { - CreateDeconstructProduct(deconstructProduct); - } + deconstructTime += targetItem.Prefab.DeconstructTime / (DeconstructionSpeed * userDeconstructorSpeedMultiplier); } - void CreateDeconstructProduct(DeconstructItem deconstructProduct) + progressState = Math.Min(progressTimer / deconstructTime, 1.0f); + if (progressTimer > deconstructTime) { - float percentageHealth = targetItem.Condition / targetItem.Prefab.Health; - if (percentageHealth <= deconstructProduct.MinCondition || percentageHealth > deconstructProduct.MaxCondition) { return; } - - if (!(MapEntityPrefab.Find(null, deconstructProduct.ItemIdentifier) is ItemPrefab itemPrefab)) + List items = inputContainer.Inventory.AllItems.ToList(); + foreach (Item targetItem in items) { - DebugConsole.ThrowError("Tried to deconstruct item \"" + targetItem.Name + "\" but couldn't find item prefab \"" + deconstructProduct.ItemIdentifier + "\"!"); - return; + if ((Entity.Spawner?.IsInRemoveQueue(targetItem) ?? false) || !inputContainer.Inventory.AllItems.Contains(targetItem)) { continue; } + 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))) && + (it.RequiredOtherItem.Length == 0 || it.RequiredOtherItem.Any(r => items.Any(it => it.HasTag(r) || it.Prefab.Identifier.Equals(r, StringComparison.OrdinalIgnoreCase))))); + + ProcessItem(targetItem, items, validDeconstructItems, allowRemove: validDeconstructItems.Any() || !targetItem.Prefab.DeconstructItems.Any()); } +#if SERVER + item.CreateServerEvent(this); +#endif + progressTimer = 0.0f; + progressState = 0.0f; - float condition = deconstructProduct.CopyCondition ? - percentageHealth * itemPrefab.Health : - itemPrefab.Health * deconstructProduct.OutCondition; + } + } + else + { + var targetItem = inputContainer.Inventory.LastOrDefault(); + if (targetItem == null) { return; } + 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; + + progressState = Math.Min(progressTimer / deconstructTime, 1.0f); + if (progressTimer > deconstructTime) + { + ProcessItem(targetItem, inputContainer.Inventory.AllItemsMod, validDeconstructItems, allowRemove: validDeconstructItems.Any() || !targetItem.Prefab.DeconstructItems.Any()); + +#if SERVER + item.CreateServerEvent(this); +#endif + progressTimer = 0.0f; + progressState = 0.0f; + + } + } + } + + private void ProcessItem(Item targetItem, IEnumerable inputItems, List validDeconstructItems, bool allowRemove = true) + { + // In multiplayer, the server handles the deconstruction into new items + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } + + if (targetItem.Prefab.RandomDeconstructionOutput) + { + int amount = targetItem.Prefab.RandomDeconstructionOutputAmount; + List deconstructItemIndexes = new List(); + for (int i = 0; i < validDeconstructItems.Count; i++) + { + deconstructItemIndexes.Add(i); + } + List commonness = validDeconstructItems.Select(i => i.Commonness).ToList(); + List products = new List(); + + for (int i = 0; i < amount; i++) + { + if (deconstructItemIndexes.Count < 1) { break; } + var itemIndex = ToolBox.SelectWeightedRandom(deconstructItemIndexes, commonness, Rand.RandSync.Unsynced); + products.Add(validDeconstructItems[itemIndex]); + var removeIndex = deconstructItemIndexes.IndexOf(itemIndex); + deconstructItemIndexes.RemoveAt(removeIndex); + commonness.RemoveAt(removeIndex); + } + + user.CheckTalents(AbilityEffectType.OnItemDeconstructed, targetItem); + + foreach (DeconstructItem deconstructProduct in products) + { + CreateDeconstructProduct(deconstructProduct, inputItems); + } + } + else + { + foreach (DeconstructItem deconstructProduct in validDeconstructItems) + { + CreateDeconstructProduct(deconstructProduct, inputItems); + } + } + + void CreateDeconstructProduct(DeconstructItem deconstructProduct, IEnumerable inputItems) + { + float percentageHealth = targetItem.Condition / targetItem.Prefab.Health; + if (percentageHealth <= deconstructProduct.MinCondition || percentageHealth > deconstructProduct.MaxCondition) { return; } + + if (!(MapEntityPrefab.Find(null, deconstructProduct.ItemIdentifier) is ItemPrefab itemPrefab)) + { + DebugConsole.ThrowError("Tried to deconstruct item \"" + targetItem.Name + "\" but couldn't find item prefab \"" + deconstructProduct.ItemIdentifier + "\"!"); + return; + } + + float condition = deconstructProduct.CopyCondition ? + percentageHealth * itemPrefab.Health : + itemPrefab.Health * Rand.Range(deconstructProduct.OutConditionMin, deconstructProduct.OutConditionMax); + + if (DeconstructItemsSimultaneously && deconstructProduct.RequiredOtherItem.Length > 0) + { + foreach (Item otherItem in inputItems) + { + if (targetItem == otherItem) { continue; } + if (deconstructProduct.RequiredOtherItem.Any(r => otherItem.HasTag(r) || r.Equals(otherItem.Prefab.Identifier, StringComparison.OrdinalIgnoreCase))) + { + var geneticMaterial1 = targetItem.GetComponent(); + var geneticMaterial2 = otherItem.GetComponent(); + if (geneticMaterial1 != null && geneticMaterial2 != null) + { + if (geneticMaterial1.Combine(geneticMaterial2, user)) + { + inputContainer.Inventory.RemoveItem(otherItem); + OutputContainer.Inventory.RemoveItem(otherItem); + Entity.Spawner.AddToRemoveQueue(otherItem); + } + allowRemove = false; + return; + } + inputContainer.Inventory.RemoveItem(otherItem); + OutputContainer.Inventory.RemoveItem(otherItem); + Entity.Spawner.AddToRemoveQueue(otherItem); + } + } + } + var itemsCreated = new AbilityValue(1f); + user.CheckTalents(AbilityEffectType.OnItemDeconstructedMaterial, (targetItem.Prefab, itemsCreated)); + + int amount = (int)itemsCreated.Value; + + for (int i = 0; i < amount; i++) + { Entity.Spawner.AddToSpawnQueue(itemPrefab, outputContainer.Inventory, condition, onSpawned: (Item spawnedItem) => { for (int i = 0; i < outputContainer.Capacity; i++) @@ -153,36 +245,31 @@ namespace Barotrauma.Items.Components PutItemsToLinkedContainer(); }); } + } - if (targetItem.Prefab.AllowDeconstruct) + if (targetItem.AllowDeconstruct && allowRemove) + { + //drop all items that are inside the deconstructed item + foreach (ItemContainer ic in targetItem.GetComponents()) { - //drop all items that are inside the deconstructed item - foreach (ItemContainer ic in targetItem.GetComponents()) - { - if (ic?.Inventory == null || ic.RemoveContainedItemsOnDeconstruct) { continue; } - ic.Inventory.AllItemsMod.ForEach(containedItem => outputContainer.Inventory.TryPutItem(containedItem, user: null)); - } - inputContainer.Inventory.RemoveItem(targetItem); - Entity.Spawner.AddToRemoveQueue(targetItem); - MoveInputQueue(); - PutItemsToLinkedContainer(); + if (ic?.Inventory == null || ic.RemoveContainedItemsOnDeconstruct) { continue; } + ic.Inventory.AllItemsMod.ForEach(containedItem => outputContainer.Inventory.TryPutItem(containedItem, user: null)); + } + inputContainer.Inventory.RemoveItem(targetItem); + Entity.Spawner.AddToRemoveQueue(targetItem); + MoveInputQueue(); + PutItemsToLinkedContainer(); + } + else + { + if (!outputContainer.Inventory.CanBePut(targetItem) || (Entity.Spawner?.IsInRemoveQueue(targetItem) ?? false)) + { + targetItem.Drop(dropper: null); } else { - if (!outputContainer.Inventory.CanBePut(targetItem)) - { - targetItem.Drop(dropper: null); - } - else - { - outputContainer.Inventory.TryPutItem(targetItem, user: null, createNetworkEvent: true); - } + outputContainer.Inventory.TryPutItem(targetItem, user: null, createNetworkEvent: true); } -#if SERVER - item.CreateServerEvent(this); -#endif - progressTimer = 0.0f; - progressState = 0.0f; } } @@ -190,7 +277,7 @@ namespace Barotrauma.Items.Components { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } if (outputContainer.Inventory.IsEmpty()) { return; } - + foreach (MapEntity linkedTo in item.linkedTo) { if (linkedTo is Item linkedItem) @@ -201,7 +288,7 @@ namespace Barotrauma.Items.Components if (itemContainer == null) { continue; } outputContainer.Inventory.AllItemsMod.ForEach(containedItem => itemContainer.Inventory.TryPutItem(containedItem, user: null, createNetworkEvent: true)); } - } + } } /// @@ -221,14 +308,54 @@ namespace Barotrauma.Items.Components } } + private IEnumerable<(Item item, DeconstructItem output)> GetAvailableOutputs(bool checkRequiredOtherItems = true) + { + var items = inputContainer.Inventory.AllItems; + foreach (Item inputItem in items) + { + if (!inputItem.AllowDeconstruct) { continue; } + foreach (var deconstructItem in inputItem.Prefab.DeconstructItems) + { + if (deconstructItem.RequiredDeconstructor.Length > 0) + { + if (!deconstructItem.RequiredDeconstructor.Any(r => item.HasTag(r) || item.Prefab.Identifier.Equals(r, StringComparison.OrdinalIgnoreCase))) { continue; } + } + if (deconstructItem.RequiredOtherItem.Length > 0 && checkRequiredOtherItems) + { + if (!deconstructItem.RequiredOtherItem.Any(r => items.Any(it => it.HasTag(r) || it.Prefab.Identifier.Equals(r, StringComparison.OrdinalIgnoreCase)))) { continue; } + bool validOtherItemFound = false; + foreach (Item otherInputItem in items) + { + if (otherInputItem == inputItem) { continue; } + if (!deconstructItem.RequiredOtherItem.Any(r => otherInputItem.HasTag(r) || otherInputItem.Prefab.Identifier.Equals(r, StringComparison.OrdinalIgnoreCase))) { continue; } + + var geneticMaterial1 = inputItem.GetComponent(); + var geneticMaterial2 = otherInputItem.GetComponent(); + if (geneticMaterial1 != null && geneticMaterial2 != null) + { + if (!geneticMaterial1.CanBeCombinedWith(geneticMaterial2)) { continue; } + } + validOtherItemFound = true; + } + if (!validOtherItemFound) { continue; } + } + yield return (inputItem, deconstructItem); + } + } + } + private void SetActive(bool active, Character user = null) { PutItemsToLinkedContainer(); + this.user = user; + if (inputContainer.Inventory.IsEmpty()) { active = false; } IsActive = active; currPowerConsumption = IsActive ? powerConsumption : 0.0f; + userDeconstructorSpeedMultiplier = user != null ? 1f + user.GetStatValue(StatTypes.DeconstructorSpeedMultiplier) : 1f; + #if SERVER if (user != null) { @@ -241,10 +368,6 @@ namespace Barotrauma.Items.Components progressState = 0.0f; } -#if CLIENT - activateButton.Text = TextManager.Get(IsActive ? "DeconstructorCancel" : "DeconstructorDeconstruct"); -#endif - inputContainer.Inventory.Locked = IsActive; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index 8d84d9d26..1ea32a0f3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -316,7 +316,7 @@ namespace Barotrauma.Items.Components availablePrefab.Condition -= availablePrefab.Prefab.Health * requiredItem.MinCondition; continue; } - + availablePrefabs.Remove(availablePrefab); Entity.Spawner.AddToRemoveQueue(availablePrefab); inputContainer.Inventory.RemoveItem(availablePrefab); @@ -324,18 +324,20 @@ namespace Barotrauma.Items.Components } }); - Character tempUser = user; - int amountFittingContainer = outputContainer.Inventory.HowManyCanBePut(fabricatedItem.TargetItem, fabricatedItem.OutCondition * fabricatedItem.TargetItem.Health); - var itemsCreated = new AbilityValue(fabricatedItem.Amount); - foreach (Character character in Character.CharacterList.Where(c => c.TeamID == user.TeamID)) + + var fabricationValueItem = new AbilityValueItem(fabricatedItem.Amount, fabricatedItem.TargetItem); + if (user != null) { - character.CheckTalents(AbilityEffectType.OnAllyItemFabricatedAmount, (fabricatedItem.TargetItem, itemsCreated)); + foreach (Character character in Character.CharacterList.Where(c => c.TeamID == user.TeamID)) + { + character.CheckTalents(AbilityEffectType.OnAllyItemFabricatedAmount, fabricationValueItem); + } + user.CheckTalents(AbilityEffectType.OnItemFabricatedAmount, fabricationValueItem); } - tempUser.CheckTalents(AbilityEffectType.OnItemFabricatedAmount, (fabricatedItem.TargetItem, itemsCreated)); - - for (int i = 0; i < (int)itemsCreated.Value; i++) + var tempUser = user; + for (int i = 0; i < (int)fabricationValueItem.Value; i++) { if (i < amountFittingContainer) { @@ -359,14 +361,13 @@ namespace Barotrauma.Items.Components } } } - if (user?.Info != null && !user.Removed) { foreach (Skill skill in fabricatedItem.RequiredSkills) { float userSkill = user.GetSkillLevel(skill.Identifier); float addedSkill = skill.Level * SkillSettings.Current.SkillIncreasePerFabricatorRequiredSkill / Math.Max(userSkill, 1.0f); - var addedSkillValue = new AbilityValue(0f); + var addedSkillValue = new AbilityValueString(0f, skill.Identifier); user.CheckTalents(AbilityEffectType.OnItemFabricationSkillGain, addedSkillValue); user.Info.IncreaseSkillLevel( diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs index 543e9bc1b..5c8e7e70e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs @@ -365,16 +365,13 @@ namespace Barotrauma.Items.Components item.SendSignal(new Signal(velY.ToString(CultureInfo.InvariantCulture), sender: user), "velocity_y_out"); // converts the controlled sub's velocity to km/h and sends it. - // TODO: add current_velocity_x and current_velocity_y pins on the navigation terminals and shuttle terminals - // TODO: increase the size of the connection panels of both navigation terminals - if (controlledSub is { } sub) { 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.RealWorldDepth.ToString("0.0000", CultureInfo.InvariantCulture), sender: user), "current_depth"); + item.SendSignal(new Signal(sub.RealWorldDepth.ToString("0.0000", CultureInfo.InvariantCulture), sender: user), "current_position_y"); } // if our tactical AI pilot has left, revert back to maintaining position diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs index a587d47ff..52c065e75 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs @@ -15,6 +15,9 @@ namespace Barotrauma.Items.Components //a list of connections a given connection is connected to, either directly or via other power transfer components private readonly Dictionary> connectedRecipients = new Dictionary>(); + private float overloadCooldownTimer; + private const float OverloadCooldown = 5.0f; + protected float powerLoad; protected bool isBroken; @@ -173,12 +176,19 @@ namespace Barotrauma.Items.Components Overload = -currPowerConsumption > Math.Max(powerLoad, 200.0f) * maxOverVoltage; if (Overload && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)) { + if (overloadCooldownTimer > 0.0f) + { + overloadCooldownTimer -= deltaTime; + return; + } + //damage the item if voltage is too high (except if running as a client) float prevCondition = item.Condition; item.Condition -= deltaTime * 10.0f; if (item.Condition <= 0.0f && prevCondition > 0.0f) { + overloadCooldownTimer = OverloadCooldown; #if CLIENT SoundPlayer.PlaySound("zap", item.WorldPosition, hullGuess: item.CurrentHull); Vector2 baseVel = Rand.Vector(300.0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/RemoteController.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/RemoteController.cs new file mode 100644 index 000000000..783253c89 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/RemoteController.cs @@ -0,0 +1,96 @@ +using Microsoft.Xna.Framework; +using System.Xml.Linq; + +namespace Barotrauma.Items.Components +{ + partial class RemoteController : ItemComponent + { + [Serialize("", false, description: "Tag or identifier of the item that should be controlled.")] + public string Target + { + get; + private set; + } + + [Serialize(false, false)] + public bool OnlyInOwnSub + { + get; + private set; + } + + [Serialize(10000.0f, false)] + public float Range + { + get; + private set; + } + + public Item TargetItem { get => currentTarget; } + + private Item currentTarget; + private Character currentUser; + private Submarine currentSub; + + public RemoteController(Item item, XElement element) + : base(item, element) + { + } + + public override bool Select(Character character) + { + if (base.Select(character)) + { + FindTarget(character); + return true; + } + return false; + } + + public override void Equip(Character character) + { + FindTarget(character); + } + + public override void Update(float deltaTime, Camera cam) + { + base.Update(deltaTime, cam); + if (currentTarget.Removed || + item.Submarine != currentSub || + Vector2.DistanceSquared(currentTarget.WorldPosition, item.WorldPosition) > Range * Range) + { + FindTarget(currentUser); + } + } + + private void FindTarget(Character user) + { + currentTarget = null; + if (user == null || (item.Submarine == null && OnlyInOwnSub)) + { + IsActive = false; + return; + } + + float closestDist = float.PositiveInfinity; + foreach (Item targetItem in Item.ItemList) + { + if (OnlyInOwnSub) + { + if (targetItem.Submarine != item.Submarine) { continue; } + if (targetItem.Submarine.TeamID != user.TeamID) { continue; } + } + if (!targetItem.HasTag(Target) && targetItem.prefab.Identifier != Target) { continue; } + + float distSqr = Vector2.DistanceSquared(item.WorldPosition, targetItem.WorldPosition); + if (distSqr > Range * Range || distSqr > closestDist) { continue; } + + currentTarget = targetItem; + currentSub = item.Submarine; + closestDist = distSqr; + currentUser = user; + } + IsActive = currentTarget != null; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs index 2b474213b..37f5833fc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs @@ -339,7 +339,7 @@ namespace Barotrauma.Items.Components if (currentFixerAction == FixActions.Tinker) { - // this is a bit code rotty to interject it here, should be less reliant on returning + // not great to interject it here, should be less reliant on returning if (!CanTinker(CurrentFixer)) { StopRepairing(CurrentFixer); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs index e7648c9ac..a52a9600b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs @@ -134,20 +134,30 @@ namespace Barotrauma.Items.Components foreach (Wire wire in c.Wires) { if (wire == null) { continue; } -#if CLIENT - if (wire.Item.IsSelected) { continue; } -#endif - var wireNodes = wire.GetNodes(); - if (wireNodes.Count == 0) { continue; } + TryMoveWire(wire); + } + } - if (Submarine.RectContains(item.Rect, wireNodes[0] + wireNodeOffset)) - { - wire.MoveNode(0, amount); - } - else if (Submarine.RectContains(item.Rect, wireNodes[wireNodes.Count - 1] + wireNodeOffset)) - { - wire.MoveNode(wireNodes.Count - 1, amount); - } + foreach (var wire in DisconnectedWires) + { + TryMoveWire(wire); + } + + void TryMoveWire(Wire wire) + { +#if CLIENT + if (wire.Item.IsSelected) { return; } +#endif + var wireNodes = wire.GetNodes(); + if (wireNodes.Count == 0) { return; } + + if (Submarine.RectContains(item.Rect, wireNodes[0] + wireNodeOffset)) + { + wire.MoveNode(0, amount); + } + else if (Submarine.RectContains(item.Rect, wireNodes[wireNodes.Count - 1] + wireNodeOffset)) + { + wire.MoveNode(wireNodes.Count - 1, amount); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs index 540284013..335827046 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs @@ -41,7 +41,7 @@ namespace Barotrauma.Items.Components set { if (string.IsNullOrEmpty(value)) { return; } - ShowOnDisplay(value); + ShowOnDisplay(value, addToHistory: true); } } @@ -59,7 +59,7 @@ namespace Barotrauma.Items.Components partial void InitProjSpecific(XElement element); - partial void ShowOnDisplay(string input, bool addToHistory = true); + partial void ShowOnDisplay(string input, bool addToHistory); public override void ReceiveSignal(Signal signal, Connection connection) { @@ -70,14 +70,14 @@ namespace Barotrauma.Items.Components } string inputSignal = signal.value.Replace("\\n", "\n"); - ShowOnDisplay(inputSignal); + ShowOnDisplay(inputSignal, addToHistory: true); } public override void OnItemLoaded() { bool isSubEditor = false; #if CLIENT - isSubEditor = Screen.Selected != GameMain.SubEditorScreen || GameMain.GameSession?.GameMode is TestGameMode; + isSubEditor = Screen.Selected == GameMain.SubEditorScreen || GameMain.GameSession?.GameMode is TestGameMode; #endif base.OnItemLoaded(); @@ -110,7 +110,7 @@ namespace Barotrauma.Items.Components { string msg = componentElement.GetAttributeString("msg" + i, null); if (msg == null) { break; } - ShowOnDisplay(msg); + ShowOnDisplay(msg, addToHistory: true); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs index 3a054c4e1..2b80a81c8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs @@ -133,15 +133,15 @@ namespace Barotrauma.Items.Components public bool IsConnectedTo(Item item) { - if (connections[0] != null && connections[0].Item == item) return true; - return (connections[1] != null && connections[1].Item == item); + if (connections[0] != null && connections[0].Item == item) { return true; } + return connections[1] != null && connections[1].Item == item; } public void RemoveConnection(Item item) { for (int i = 0; i < 2; i++) { - if (connections[i] == null || connections[i].Item != item) continue; + if (connections[i] == null || connections[i].Item != item) { continue; } foreach (Wire wire in connections[i].Wires) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index 0bed7d299..ecb5d1cf4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -64,6 +64,8 @@ namespace Barotrauma.Items.Components private Character currentTarget; const float aiFindTargetInterval = 5.0f; + private const float TinkeringPowerCostReduction = 1.25f; + public float Rotation { get { return rotation; } @@ -504,9 +506,19 @@ namespace Barotrauma.Items.Components return TryLaunch(deltaTime, character); } + public float GetPowerRequiredToShoot() + { + float powerCost = powerConsumption; + if (user != null) + { + powerCost /= (1 + user.GetStatValue(StatTypes.TurretPowerCostReduction)); + } + return powerCost; + } + public bool HasPowerToShoot() { - return GetAvailableBatteryPower() >= powerConsumption; + return GetAvailableBatteryPower() >= GetPowerRequiredToShoot(); } private bool TryLaunch(float deltaTime, Character character = null, bool ignorePower = false) @@ -617,10 +629,12 @@ namespace Barotrauma.Items.Components if (!ignorePower) { var batteries = item.GetConnectedComponents(); - float neededPower = powerConsumption; + 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 /= 1.25f; + neededPower /= TinkeringPowerCostReduction; } while (neededPower > 0.0001f && batteries.Count > 0) { @@ -1022,7 +1036,7 @@ namespace Barotrauma.Items.Components container = containerItem.GetComponent(); if (container != null) { break; } } - if (container == null || container.ContainableItems.Count == 0) + if (container == null || !container.ContainableItemIdentifiers.Any()) { if (character.IsOnPlayerTeam) { @@ -1046,7 +1060,7 @@ namespace Barotrauma.Items.Components { if (!character.IsOnPlayerTeam) { return; } if (character.Submarine != Submarine.MainSub) { return; } - string ammoType = container.ContainableItems.First().Identifiers.FirstOrDefault() ?? "ammobox"; + string ammoType = container.ContainableItemIdentifiers.FirstOrDefault() ?? "ammobox"; int remainingAmmo = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(ammoType) && i.Condition > 1); if (remainingAmmo == 0) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs index baec2c0b0..be1b859b6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs @@ -55,6 +55,8 @@ namespace Barotrauma public float Scale { get; private set; } + public float Rotation { get; private set; } + public LimbType DepthLimb { get; private set; } private Wearable _wearableComponent; public Wearable WearableComponent @@ -177,6 +179,7 @@ namespace Barotrauma DepthLimb = (LimbType)Enum.Parse(typeof(LimbType), SourceElement.GetAttributeString("depthlimb", "None"), true); Sound = SourceElement.GetAttributeString("sound", ""); Scale = SourceElement.GetAttributeFloat("scale", 1.0f); + Rotation = MathHelper.ToRadians(SourceElement.GetAttributeFloat("rotation", 0.0f)); var index = SourceElement.GetAttributePoint("sheetindex", new Point(-1, -1)); if (index.X > -1 && index.Y > -1) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs index a259d4246..f422df2e8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -496,7 +496,7 @@ namespace Barotrauma var itemInSlot = slots[i].First(); if (itemInSlot.OwnInventory != null && !itemInSlot.OwnInventory.Contains(item) && - (itemInSlot.GetComponent()?.MaxStackSize ?? 0) == 1 && + (itemInSlot.GetComponent()?.GetMaxStackSize(0) ?? 0) == 1 && itemInSlot.OwnInventory.TrySwapping(0, item, user, createNetworkEvent, swapWholeStack: false)) { return true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index db66ce9f0..14fb5af13 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -540,6 +540,12 @@ namespace Barotrauma set => indestructible = value; } + public bool AllowDeconstruct + { + get; + set; + } + [Editable, Serialize(false, isSaveable: true, "When enabled will prevent the item from taking damage from all sources")] public bool InvulnerableToDamage { get; set; } @@ -767,6 +773,8 @@ namespace Barotrauma condition = MaxCondition; lastSentCondition = condition; + AllowDeconstruct = itemPrefab.AllowDeconstruct; + allPropertyObjects.Add(this); XElement element = itemPrefab.ConfigElement; @@ -1431,7 +1439,7 @@ namespace Barotrauma bool hasTargets = effect.TargetIdentifiers == null; targets.Clear(); - + if (effect.HasTargetType(StatusEffect.TargetType.Contained)) { foreach (Item containedItem in ContainedItems) @@ -1443,6 +1451,11 @@ namespace Barotrauma continue; } + if (effect.TargetSlot > -1) + { + if (OwnInventory.FindIndex(containedItem) != effect.TargetSlot) { continue; } + } + hasTargets = true; targets.Add(containedItem); } @@ -1500,8 +1513,8 @@ namespace Barotrauma { targets.Add(limb); } - - if (Container != null && effect.HasTargetType(StatusEffect.TargetType.Parent)) targets.Add(Container); + + if (Container != null && effect.HasTargetType(StatusEffect.TargetType.Parent)) { targets.Add(Container); } effect.Apply(type, deltaTime, this, targets, worldPosition); } @@ -2298,8 +2311,8 @@ namespace Barotrauma public void ApplyTreatment(Character user, Character character, Limb targetLimb) { //can't apply treatment to dead characters - if (character.IsDead) return; - if (!UseInHealthInterface) return; + if (character.IsDead) { return; } + if (!UseInHealthInterface) { return; } #if CLIENT if (GameMain.Client != null) @@ -2312,7 +2325,7 @@ namespace Barotrauma bool remove = false; foreach (ItemComponent ic in components) { - if (!ic.HasRequiredContainedItems(user, addMessage: user == Character.Controlled)) continue; + if (!ic.HasRequiredContainedItems(user, addMessage: user == Character.Controlled)) { continue; } bool success = Rand.Range(0.0f, 0.5f) < ic.DegreeOfSuccess(user); ActionType actionType = success ? ActionType.OnUse : ActionType.OnFailure; @@ -2331,7 +2344,7 @@ namespace Barotrauma }); } - if (ic.DeleteOnUse) remove = true; + if (ic.DeleteOnUse) { remove = true; } } if (remove) { Spawner?.AddToRemoveQueue(this); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs index d449ad8c9..bcefbec04 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemInventory.cs @@ -9,7 +9,7 @@ namespace Barotrauma { partial class ItemInventory : Inventory { - private ItemContainer container; + private readonly ItemContainer container; public ItemContainer Container { get { return container; } @@ -48,14 +48,14 @@ namespace Barotrauma if (ItemOwnsSelf(item)) { return false; } if (i < 0 || i >= slots.Length) { return false; } if (!container.CanBeContained(item)) { return false; } - return item != null && slots[i].CanBePut(item, ignoreCondition) && slots[i].ItemCount < container.MaxStackSize; + return item != null && slots[i].CanBePut(item, ignoreCondition) && slots[i].ItemCount < container.GetMaxStackSize(i); } public override bool CanBePutInSlot(ItemPrefab itemPrefab, int i, float? condition) { if (i < 0 || i >= slots.Length) { return false; } if (!container.CanBeContained(itemPrefab)) { return false; } - return itemPrefab != null && slots[i].CanBePut(itemPrefab, condition) && slots[i].ItemCount < container.MaxStackSize; + return itemPrefab != null && slots[i].CanBePut(itemPrefab, condition) && slots[i].ItemCount < container.GetMaxStackSize(i); } public override int HowManyCanBePut(ItemPrefab itemPrefab, int i, float? condition) @@ -63,7 +63,7 @@ namespace Barotrauma if (itemPrefab == null) { return 0; } if (i < 0 || i >= slots.Length) { return 0; } if (!container.CanBeContained(itemPrefab)) { return 0; } - return slots[i].HowManyCanBePut(itemPrefab, maxStackSize: Math.Min(itemPrefab.MaxStackSize, container.MaxStackSize), condition); + return slots[i].HowManyCanBePut(itemPrefab, maxStackSize: Math.Min(itemPrefab.MaxStackSize, container.GetMaxStackSize(i)), condition); } public override bool IsFull(bool takeStacksIntoAccount = false) @@ -74,7 +74,7 @@ namespace Barotrauma { if (!slots[i].Any()) { return false; } var item = slots[i].FirstOrDefault(); - if (slots[i].ItemCount < Math.Min(item.Prefab.MaxStackSize, container.MaxStackSize)) { return false; } + if (slots[i].ItemCount < Math.Min(item.Prefab.MaxStackSize, container.GetMaxStackSize(i))) { return false; } } } else diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index e2c81690d..67a117dde 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -18,9 +18,18 @@ namespace Barotrauma //maxCondition does > check, meaning that above this max the deconstruct item will be skipped. public readonly float MaxCondition; //Condition of item on creation - public readonly float OutCondition; + public readonly float OutConditionMin, OutConditionMax; //should the condition of the deconstructed item be copied to the output items public readonly bool CopyCondition; + //tag/identifier of the deconstructor(s) that can be used to deconstruct the item into this + public readonly string[] RequiredDeconstructor; + //tag/identifier of other item(s) that that need to be present in the deconstructor to deconstruct the item into this + public readonly string[] RequiredOtherItem; + //text to display on the deconstructor's activate button when this output is available + public readonly string ActivateButtonText; + public readonly string InfoText; + public readonly string InfoTextOnOtherItemMissing; + public float Commonness { get; } public DeconstructItem(XElement element, string parentDebugName) @@ -28,14 +37,20 @@ namespace Barotrauma ItemIdentifier = element.GetAttributeString("identifier", "notfound"); MinCondition = element.GetAttributeFloat("mincondition", -0.1f); MaxCondition = element.GetAttributeFloat("maxcondition", 1.0f); - OutCondition = element.GetAttributeFloat("outcondition", 1.0f); + OutConditionMin = element.GetAttributeFloat("outconditionmin", element.GetAttributeFloat("outcondition", 1.0f)); + OutConditionMax = element.GetAttributeFloat("outconditionmax", element.GetAttributeFloat("outcondition", 1.0f)); CopyCondition = element.GetAttributeBool("copycondition", false); Commonness = element.GetAttributeFloat("commonness", 1.0f); - if (element.Attribute("copycondition") != null && element.Attribute("outcondition") != null) { DebugConsole.AddWarning($"Invalid deconstruction output in \"{parentDebugName}\": the output item \"{ItemIdentifier}\" has the out condition set, but is also set to copy the condition of the deconstructed item. Ignoring the out condition."); } + RequiredDeconstructor = element.GetAttributeStringArray("requireddeconstructor", new string[0]); + RequiredOtherItem = element.GetAttributeStringArray("requiredotheritem", new string[0]); + ActivateButtonText = element.GetAttributeString("activatebuttontext", string.Empty); + InfoText = element.GetAttributeString("infotext", string.Empty); + InfoTextOnOtherItemMissing = element.GetAttributeString("infotextonotheritemmissing", string.Empty); + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index a1958db8e..4b657de2a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -33,6 +33,9 @@ namespace Barotrauma private readonly float? flashRange; private readonly string decal; private readonly float decalSize; + // used to apply friendly afflictions in an area without effects displaying + private readonly bool abilityExplosion; + private readonly bool applyToSelf; private readonly float itemRepairStrength; @@ -63,8 +66,10 @@ namespace Barotrauma force = element.GetAttributeFloat("force", 0.0f); - bool showEffects = element.GetAttributeBool("showeffects", true); + abilityExplosion = element.GetAttributeBool("abilityexplosion", false); + applyToSelf = element.GetAttributeBool("applytoself", true); + bool showEffects = !abilityExplosion; sparks = element.GetAttributeBool("sparks", showEffects); shockwave = element.GetAttributeBool("shockwave", showEffects); flames = element.GetAttributeBool("flames", showEffects); @@ -191,12 +196,12 @@ namespace Barotrauma } } - if (MathUtils.NearlyEqual(force, 0.0f) && MathUtils.NearlyEqual(Attack.Stun, 0.0f) && MathUtils.NearlyEqual(Attack.GetTotalDamage(false), 0.0f)) + if (MathUtils.NearlyEqual(force, 0.0f) && MathUtils.NearlyEqual(Attack.Stun, 0.0f) && MathUtils.NearlyEqual(Attack.GetTotalDamage(false), 0.0f) && !abilityExplosion) { return; } - DamageCharacters(worldPosition, Attack, force, damageSource, attacker); + DamageCharacters(worldPosition, Attack, force, damageSource, attacker, applyToSelf); if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) { @@ -250,7 +255,7 @@ namespace Barotrauma partial void ExplodeProjSpecific(Vector2 worldPosition, Hull hull); - private void DamageCharacters(Vector2 worldPosition, Attack attack, float force, Entity damageSource, Character attacker) + private void DamageCharacters(Vector2 worldPosition, Attack attack, float force, Entity damageSource, Character attacker, bool applyToSelf) { if (attack.Range <= 0.0f) { return; } @@ -265,6 +270,8 @@ namespace Barotrauma { continue; } + if (c == attacker && !applyToSelf) { continue; } + if (onlyInside && c.Submarine == null) { continue; } else if (onlyOutside && c.Submarine != null) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index afb5f6086..e32aa7ed7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -3641,9 +3641,10 @@ namespace Barotrauma } if (LevelData.IsBeaconActive) { - if (reactorContainer != null && reactorContainer.Inventory.IsEmpty()) + if (reactorContainer != null && reactorContainer.Inventory.IsEmpty() && + reactorContainer.ContainableItemIdentifiers.Any() && ItemPrefab.Prefabs.ContainsKey(reactorContainer.ContainableItemIdentifiers.FirstOrDefault())) { - ItemPrefab fuelPrefab = ItemPrefab.Prefabs[reactorContainer.ContainableItems[0].Identifiers[0]]; + ItemPrefab fuelPrefab = ItemPrefab.Prefabs[reactorContainer.ContainableItemIdentifiers.FirstOrDefault()]; Spawner.AddToSpawnQueue( fuelPrefab, reactorContainer.Inventory, onSpawned: (it) => reactorComponent.PowerUpImmediately()); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index 80ba583ee..f099563c6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -203,8 +203,8 @@ namespace Barotrauma if (!ResizeHorizontal || !ResizeVertical) { - int newWidth = ResizeHorizontal ? rect.Width : (int)(defaultRect.Width * relativeScale); - int newHeight = ResizeVertical ? rect.Height : (int)(defaultRect.Height * relativeScale); + int newWidth = Math.Max(ResizeHorizontal ? rect.Width : (int)(defaultRect.Width * relativeScale), 1); + int newHeight = Math.Max(ResizeVertical ? rect.Height : (int)(defaultRect.Height * relativeScale), 1); Rect = new Rectangle(rect.X, rect.Y, newWidth, newHeight); if (StairDirection != Direction.None) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index faa57d7b1..d9ae32ee1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -223,6 +223,11 @@ namespace Barotrauma private readonly TargetType targetTypes; protected HashSet targetIdentifiers; + /// + /// Index of the slot the target must be in when targeting a Contained item + /// + public int TargetSlot = -1; + private readonly List requiredItems; public readonly string[] propertyNames; @@ -262,7 +267,9 @@ namespace Barotrauma public readonly List Explosions; private readonly List spawnItems; + private readonly bool spawnItemRandomly; private readonly List spawnCharacters; + private readonly List aiTriggers; private readonly List triggeredEvents; @@ -294,7 +301,10 @@ namespace Barotrauma get { return targetIdentifiers; } } - public HashSet AllowedAfflictions { get; private set; } + /// + /// Which type of afflictions the target must receive for the StatusEffect to be applied. Only valid when the type of the effect is OnDamaged. + /// + private readonly HashSet<(string affliction, float strength)> requiredAfflictions; public List Afflictions { @@ -307,7 +317,7 @@ namespace Barotrauma get { return spawnCharacters; } } - public readonly List> ReduceAffliction; + public readonly List<(string affliction, float amount)> ReduceAffliction; private readonly List giveExperiences; private readonly List<(string identifier, float amount)> giveSkills; @@ -354,12 +364,13 @@ namespace Barotrauma { requiredItems = new List(); spawnItems = new List(); + spawnItemRandomly = element.GetAttributeBool("spawnitemrandomly", false); spawnCharacters = new List(); aiTriggers = new List(); Afflictions = new List(); Explosions = new List(); triggeredEvents = new List(); - ReduceAffliction = new List>(); + ReduceAffliction = new List<(string affliction, float amount)>(); giveExperiences = new List(); giveSkills = new List<(string, float)>(); @@ -369,6 +380,8 @@ namespace Barotrauma OnlyPlayerTriggered = element.GetAttributeBool("onlyplayertriggered", false); AllowWhenBroken = element.GetAttributeBool("allowwhenbroken", false); + TargetSlot = element.GetAttributeInt("targetslot", -1); + Range = element.GetAttributeFloat("range", 0.0f); Offset = element.GetAttributeVector2("offset", Vector2.Zero); string[] targetLimbNames = element.GetAttributeStringArray("targetlimb", null) ?? element.GetAttributeStringArray("targetlimbs", null); @@ -436,11 +449,12 @@ namespace Barotrauma } break; case "allowedafflictions": + case "requiredafflictions": string[] types = attribute.Value.Split(','); - AllowedAfflictions = new HashSet(); + requiredAfflictions ??= new HashSet<(string, float)>(); for (int i = 0; i < types.Length; i++) { - AllowedAfflictions.Add(types[i].Trim().ToLowerInvariant()); + requiredAfflictions.Add((types[i].Trim().ToLowerInvariant(), 0.0f)); } break; case "duration": @@ -551,6 +565,13 @@ namespace Barotrauma } requiredItems.Add(newRequiredItem); break; + case "requiredaffliction": + + requiredAfflictions ??= new HashSet<(string, float)>(); + requiredAfflictions.Add(( + subElement.GetAttributeString("identifier", string.Empty), + subElement.GetAttributeFloat("minstrength", 0.0f))); + break; case "conditional": foreach (XAttribute attribute in subElement.Attributes()) { @@ -593,7 +614,7 @@ namespace Barotrauma if (subElement.Attribute("name") != null) { DebugConsole.ThrowError("Error in StatusEffect (" + parentDebugName + ") - define afflictions using identifiers or types instead of names."); - ReduceAffliction.Add(new Pair( + ReduceAffliction.Add(( subElement.GetAttributeString("name", "").ToLowerInvariant(), subElement.GetAttributeFloat(1.0f, "amount", "strength", "reduceamount"))); } @@ -604,9 +625,7 @@ namespace Barotrauma if (AfflictionPrefab.List.Any(ap => ap.Identifier == name || ap.AfflictionType == name)) { - ReduceAffliction.Add(new Pair( - name, - subElement.GetAttributeFloat(1.0f, "amount", "strength", "reduceamount"))); + ReduceAffliction.Add((name, subElement.GetAttributeFloat(1.0f, "amount", "strength", "reduceamount"))); } else { @@ -672,6 +691,17 @@ namespace Barotrauma return false; } + public bool HasRequiredAfflictions(AttackResult attackResult) + { + if (requiredAfflictions == null) { return true; } + if (attackResult.Afflictions == null) { return false; } + if (attackResult.Afflictions.None(a => requiredAfflictions.Any(a2 => a.Strength >= a2.strength && a.Identifier == a2.affliction || a.Prefab.AfflictionType == a2.affliction))) + { + return false; + } + return true; + } + public virtual bool HasRequiredItems(Entity entity) { if (entity == null) { return true; } @@ -1118,13 +1148,10 @@ namespace Barotrauma { if (Rand.Value(Rand.RandSync.Unsynced) > affliction.Probability) { continue; } Affliction newAffliction = affliction; - if (!disableDeltaTime && !setValue) - { - newAffliction = affliction.CreateMultiplied(deltaTime); - } if (target is Character character) { if (character.Removed) { continue; } + newAffliction = GetMultipliedAffliction(affliction, entity, character, deltaTime); character.LastDamageSource = entity; foreach (Limb limb in character.AnimController.Limbs) { @@ -1133,6 +1160,7 @@ namespace Barotrauma if (targetLimbs != null && !targetLimbs.Contains(limb.type)) { continue; } AttackResult result = limb.character.DamageLimb(position, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: affliction.Source, allowStacking: !setValue); limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability, disableDeltaTime ? result.Damage : result.Damage / deltaTime, allowBeheading: true); + RegisterTreatmentResults(entity, limb, affliction, result); //only apply non-limb-specific afflictions to the first limb if (!affliction.Prefab.LimbSpecific) { break; } } @@ -1141,14 +1169,15 @@ namespace Barotrauma { if (limb.IsSevered) { continue; } if (limb.character.Removed || limb.Removed) { continue; } + newAffliction = GetMultipliedAffliction(affliction, entity, limb.character, deltaTime); AttackResult result = limb.character.DamageLimb(position, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: affliction.Source, allowStacking: !setValue); limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability, disableDeltaTime ? result.Damage : result.Damage / deltaTime, allowBeheading: true); + RegisterTreatmentResults(entity, limb, affliction, result); } } - foreach (Pair reduceAffliction in ReduceAffliction) + foreach (var (affliction, amount) in ReduceAffliction) { - float reduceAmount = disableDeltaTime || setValue ? reduceAffliction.Second : reduceAffliction.Second * deltaTime; Limb targetLimb = null; Character targetCharacter = null; if (target is Character character) @@ -1162,8 +1191,11 @@ namespace Barotrauma } if (targetCharacter != null && !targetCharacter.Removed) { + ActionType? actionType = null; + if (entity is Item item && item.UseInHealthInterface) { actionType = type; } + float reduceAmount = amount * GetAfflictionMultiplier(entity, targetCharacter, deltaTime); float prevVitality = targetCharacter.Vitality; - targetCharacter.CharacterHealth.ReduceAffliction(targetLimb, reduceAffliction.First, reduceAmount); + targetCharacter.CharacterHealth.ReduceAffliction(targetLimb, affliction, reduceAmount, treatmentAction: actionType); if (user != null && user != targetCharacter) { if (!targetCharacter.IsDead) @@ -1305,111 +1337,124 @@ namespace Barotrauma }); } } - foreach (ItemSpawnInfo itemSpawnInfo in spawnItems) + if (spawnItemRandomly) { - for (int i = 0; i < itemSpawnInfo.Count; i++) + SpawnItem(spawnItems.GetRandom()); + } + else + { + foreach (ItemSpawnInfo itemSpawnInfo in spawnItems) { - switch (itemSpawnInfo.SpawnPosition) + for (int i = 0; i < itemSpawnInfo.Count; i++) { - case ItemSpawnInfo.SpawnPositionType.This: - Entity.Spawner.AddToSpawnQueue(itemSpawnInfo.ItemPrefab, position + Rand.Vector(itemSpawnInfo.Spread, Rand.RandSync.Server), onSpawned: newItem => - { - Projectile projectile = newItem.GetComponent(); - if (projectile != null && user != null && sourceBody != null && entity != null) - { - var rope = newItem.GetComponent(); - if (rope != null && sourceBody.UserData is Limb sourceLimb) - { - rope.Attach(sourceLimb, newItem); - } - - float spread = MathHelper.ToRadians(Rand.Range(-itemSpawnInfo.AimSpread, itemSpawnInfo.AimSpread)); - var worldPos = sourceBody.Position; - float rotation = itemSpawnInfo.Rotation; - if (user.Submarine != null) - { - worldPos += user.Submarine.Position; - } - switch (itemSpawnInfo.RotationType) - { - case ItemSpawnInfo.SpawnRotationType.Fixed: - rotation = sourceBody.TransformRotation(itemSpawnInfo.Rotation); - break; - case ItemSpawnInfo.SpawnRotationType.Target: - rotation = MathUtils.VectorToAngle(entity.WorldPosition - worldPos); - break; - case ItemSpawnInfo.SpawnRotationType.Limb: - rotation = sourceBody.TransformedRotation; - break; - case ItemSpawnInfo.SpawnRotationType.Collider: - rotation = user.AnimController.Collider.Rotation; - break; - case ItemSpawnInfo.SpawnRotationType.MainLimb: - rotation = user.AnimController.MainLimb.body.TransformedRotation; - break; - default: - throw new NotImplementedException("Not implemented: " + itemSpawnInfo.RotationType); - } - rotation += MathHelper.ToRadians(itemSpawnInfo.Rotation * user.AnimController.Dir); - projectile.Shoot(user, ConvertUnits.ToSimUnits(worldPos), ConvertUnits.ToSimUnits(worldPos), rotation + spread, ignoredBodies: user.AnimController.Limbs.Where(l => !l.IsSevered).Select(l => l.body.FarseerBody).ToList(), createNetworkEvent: true); - } - else - { - newItem.body?.ApplyLinearImpulse(Rand.Vector(1) * itemSpawnInfo.Speed); - newItem.Rotation = itemSpawnInfo.Rotation; - } - }); - break; - case ItemSpawnInfo.SpawnPositionType.ThisInventory: - { - Inventory inventory = null; - if (entity is Character character && character.Inventory != null) - { - inventory = character.Inventory; - } - else if (entity is Item item) - { - inventory = item?.GetComponent()?.Inventory; - } - if (inventory != null && inventory.CanBePut(itemSpawnInfo.ItemPrefab)) - { - Entity.Spawner.AddToSpawnQueue(itemSpawnInfo.ItemPrefab, inventory, spawnIfInventoryFull: false); - } - } - break; - case ItemSpawnInfo.SpawnPositionType.ContainedInventory: - { - Inventory thisInventory = null; - if (entity is Character character) - { - thisInventory = character.Inventory; - } - else if (entity is Item item) - { - thisInventory = item?.GetComponent()?.Inventory; - } - if (thisInventory != null) - { - foreach (Item item in thisInventory.AllItems) - { - Inventory containedInventory = item.GetComponent()?.Inventory; - if (containedInventory != null && containedInventory.CanBePut(itemSpawnInfo.ItemPrefab)) - { - Entity.Spawner.AddToSpawnQueue(itemSpawnInfo.ItemPrefab, containedInventory, spawnIfInventoryFull: false); - } - break; - } - } - } - break; + SpawnItem(itemSpawnInfo); } } } + + + void SpawnItem(ItemSpawnInfo chosenItemSpawnInfo) + { + switch (chosenItemSpawnInfo.SpawnPosition) + { + case ItemSpawnInfo.SpawnPositionType.This: + Entity.Spawner.AddToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, position + Rand.Vector(chosenItemSpawnInfo.Spread, Rand.RandSync.Server), onSpawned: newItem => + { + Projectile projectile = newItem.GetComponent(); + if (projectile != null && user != null && sourceBody != null && entity != null) + { + var rope = newItem.GetComponent(); + if (rope != null && sourceBody.UserData is Limb sourceLimb) + { + rope.Attach(sourceLimb, newItem); + } + + float spread = MathHelper.ToRadians(Rand.Range(-chosenItemSpawnInfo.AimSpread, chosenItemSpawnInfo.AimSpread)); + var worldPos = sourceBody.Position; + float rotation = chosenItemSpawnInfo.Rotation; + if (user.Submarine != null) + { + worldPos += user.Submarine.Position; + } + switch (chosenItemSpawnInfo.RotationType) + { + case ItemSpawnInfo.SpawnRotationType.Fixed: + rotation = sourceBody.TransformRotation(chosenItemSpawnInfo.Rotation); + break; + case ItemSpawnInfo.SpawnRotationType.Target: + rotation = MathUtils.VectorToAngle(entity.WorldPosition - worldPos); + break; + case ItemSpawnInfo.SpawnRotationType.Limb: + rotation = sourceBody.TransformedRotation; + break; + case ItemSpawnInfo.SpawnRotationType.Collider: + rotation = user.AnimController.Collider.Rotation; + break; + case ItemSpawnInfo.SpawnRotationType.MainLimb: + rotation = user.AnimController.MainLimb.body.TransformedRotation; + break; + default: + throw new NotImplementedException("Not implemented: " + chosenItemSpawnInfo.RotationType); + } + rotation += MathHelper.ToRadians(chosenItemSpawnInfo.Rotation * user.AnimController.Dir); + projectile.Shoot(user, ConvertUnits.ToSimUnits(worldPos), ConvertUnits.ToSimUnits(worldPos), rotation + spread, ignoredBodies: user.AnimController.Limbs.Where(l => !l.IsSevered).Select(l => l.body.FarseerBody).ToList(), createNetworkEvent: true); + } + else + { + newItem.body?.ApplyLinearImpulse(Rand.Vector(1) * chosenItemSpawnInfo.Speed); + newItem.Rotation = chosenItemSpawnInfo.Rotation; + } + }); + break; + case ItemSpawnInfo.SpawnPositionType.ThisInventory: + { + Inventory inventory = null; + if (entity is Character character && character.Inventory != null) + { + inventory = character.Inventory; + } + else if (entity is Item item) + { + inventory = item?.GetComponent()?.Inventory; + } + if (inventory != null && inventory.CanBePut(chosenItemSpawnInfo.ItemPrefab)) + { + Entity.Spawner.AddToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, inventory, spawnIfInventoryFull: false); + } + } + break; + case ItemSpawnInfo.SpawnPositionType.ContainedInventory: + { + Inventory thisInventory = null; + if (entity is Character character) + { + thisInventory = character.Inventory; + } + else if (entity is Item item) + { + thisInventory = item?.GetComponent()?.Inventory; + } + if (thisInventory != null) + { + foreach (Item item in thisInventory.AllItems) + { + Inventory containedInventory = item.GetComponent()?.Inventory; + if (containedInventory != null && containedInventory.CanBePut(chosenItemSpawnInfo.ItemPrefab)) + { + Entity.Spawner.AddToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, containedInventory, spawnIfInventoryFull: false); + } + break; + } + } + } + break; + } + } } ApplyProjSpecific(deltaTime, entity, targets, hull, position, playSound: true); - Character CharacterFromTarget(ISerializableEntity target) + static Character CharacterFromTarget(ISerializableEntity target) { Character targetCharacter = target as Character; if (targetCharacter == null) @@ -1494,22 +1539,24 @@ namespace Barotrauma foreach (Affliction affliction in element.Parent.Afflictions) { - Affliction multipliedAffliction = affliction; - if (!element.Parent.disableDeltaTime && !element.Parent.setValue) { multipliedAffliction = affliction.CreateMultiplied(deltaTime); } - + Affliction newAffliction = affliction; if (target is Character character) { if (character.Removed) { continue; } - character.AddDamage(character.WorldPosition, multipliedAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attacker: element.User); + newAffliction = element.Parent.GetMultipliedAffliction(affliction, element.Entity, character, deltaTime); + var result = character.AddDamage(character.WorldPosition, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attacker: element.User); + element.Parent.RegisterTreatmentResults(element.Entity, result.HitLimb, affliction, result); } else if (target is Limb limb) { if (limb.character.Removed || limb.Removed) { continue; } - limb.character.DamageLimb(limb.WorldPosition, limb, multipliedAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: element.User); + newAffliction = element.Parent.GetMultipliedAffliction(affliction, element.Entity, limb.character, deltaTime); + var result = limb.character.DamageLimb(limb.WorldPosition, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: element.User); + element.Parent.RegisterTreatmentResults(element.Entity, limb, affliction, result); } } - foreach (Pair reduceAffliction in element.Parent.ReduceAffliction) + foreach (var (affliction, amount) in element.Parent.ReduceAffliction) { Limb targetLimb = null; Character targetCharacter = null; @@ -1524,8 +1571,11 @@ namespace Barotrauma } if (targetCharacter != null && !targetCharacter.Removed) { + ActionType? actionType = null; + if (element.Entity is Item item && item.UseInHealthInterface) { actionType = element.Parent.type; } + float reduceAmount = amount * element.Parent.GetAfflictionMultiplier(element.Entity, targetCharacter, deltaTime); float prevVitality = targetCharacter.Vitality; - targetCharacter.CharacterHealth.ReduceAffliction(targetLimb, reduceAffliction.First, reduceAffliction.Second * deltaTime); + targetCharacter.CharacterHealth.ReduceAffliction(targetLimb, affliction, reduceAmount, treatmentAction: actionType); if (element.User != null && element.User != targetCharacter) { if (!targetCharacter.IsDead) @@ -1554,6 +1604,48 @@ namespace Barotrauma } } + private float GetAfflictionMultiplier(Entity entity, Character targetCharacter, float deltaTime) + { + float multiplier = !setValue && !disableDeltaTime ? deltaTime : 1.0f; + if (entity is Item sourceItem && sourceItem.HasTag("medical")) + { + multiplier *= 1 + targetCharacter.GetStatValue(StatTypes.MedicalItemEffectivenessMultiplier); + } + return multiplier; + } + + private Affliction GetMultipliedAffliction(Affliction affliction, Entity entity, Character targetCharacter, float deltaTime) + { + float afflictionMultiplier = GetAfflictionMultiplier(entity, targetCharacter, deltaTime); + if (!MathUtils.NearlyEqual(afflictionMultiplier, 1.0f)) + { + return affliction.CreateMultiplied(afflictionMultiplier); + } + return affliction; + } + + private void RegisterTreatmentResults(Entity entity, Limb limb, Affliction affliction, AttackResult result) + { + if (entity is Item item && item.UseInHealthInterface) + { + foreach (Affliction limbAffliction in limb.character.CharacterHealth.GetAllAfflictions()) + { + if (result.Afflictions.Any(a => a.Prefab == limbAffliction.Prefab) && + (!affliction.Prefab.LimbSpecific || limb.character.CharacterHealth.GetAfflictionLimb(affliction) == limb)) + { + if (type == ActionType.OnUse) + { + limbAffliction.AppliedAsSuccessfulTreatmentTime = Timing.TotalTime; + } + else if (type == ActionType.OnFailure) + { + limbAffliction.AppliedAsFailedTreatmentTime = Timing.TotalTime; + } + } + } + } + } + static partial void UpdateAllProjSpecific(float deltaTime); public static void StopAll() diff --git a/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs b/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs index c7aa407ea..06710d345 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Text; using System.Text.RegularExpressions; using Barotrauma.Extensions; +using System.Xml.Linq; namespace Barotrauma { @@ -443,6 +444,42 @@ namespace Barotrauma } } + /// + /// Constructs a string from XML in a way that allows replacing one or more variables with hard-coded or localized values. Usage example in the method's comments. + /// + public static void ConstructDescription(ref string Description, XElement descriptionElement) + { + /* + + + + + + */ + + string extraDescriptionLine = Get(descriptionElement.GetAttributeString("tag", string.Empty)); + if (string.IsNullOrEmpty(extraDescriptionLine)) { return; } + foreach (XElement replaceElement in descriptionElement.Elements()) + { + if (replaceElement.Name.ToString().ToLowerInvariant() != "replace") { continue; } + + string tag = replaceElement.GetAttributeString("tag", string.Empty); + string[] replacementValues = replaceElement.GetAttributeStringArray("value", new string[0]); + string replacementValue = string.Empty; + for (int i = 0; i < replacementValues.Length; i++) + { + replacementValue += Get(replacementValues[i], returnNull: true) ?? replacementValues[i]; + if (i < replacementValues.Length - 1) + { + replacementValue += ", "; + } + } + extraDescriptionLine = extraDescriptionLine.Replace(tag, replacementValue); + } + if (!string.IsNullOrEmpty(Description)) { Description += "\n"; } + Description += extraDescriptionLine; + } + public static string FormatServerMessage(string textId) { return $"{textId}~"; diff --git a/Barotrauma/BarotraumaShared/Submarines/Humpback.sub b/Barotrauma/BarotraumaShared/Submarines/Humpback.sub index 79b880af6..90a86631d 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Humpback.sub and b/Barotrauma/BarotraumaShared/Submarines/Humpback.sub differ diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index f5866ba31..48c9afcb0 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,39 @@ +--------------------------------------------------------------------------------------------------------- +v0.1500.1.0 +--------------------------------------------------------------------------------------------------------- + +Additions and changes: +- Gene splicing. You can find alien genetic material inside ruins (and for the time being, wrecks), and use these materials to gain special abilities and buffs. The materials can be processed using a Research Station (which atm can be found in research outpost) and applied on a character using a Gene Splicer. +- Added WIP talent trees for security officers and assistants. +- Streamlined the health interface. +- Allow administering meds by clicking on the "suitable treatments" suggestions in the health interface. +- Physical injuries to the head can cause concussions. +- Play editor music in multiplayer lobby. +- Option to specify the amount of items to spawn with the "spawnitem" command. +- Optimized cave vent and ballast flora spore particles. +- Added a 5 second "cooldown" before a junction box broken by overloading can take damage from overloading again. Prevents continuous fires and particles when continuously repairing an overloaded junction box. +- Small monsters don't eat the inventory contents of a character they're eating (the items drop instead). +- Disabled new status monitor features from handheld status monitors. +- Round water and oxygen percentage readings on the status monitor (e.g. 99.999998% shows up as 100% instead of 99%). + +Fixes: +- Fixed crashing when an attack is applied on a character from a source other than another character, e.g. propeller (unstable only). +- Fixed current_position_y output not working on nav terminals (unstable only). +- Fixed fuel rods having a bullet as a contained indicator (unstable only). +- Removed duplicate welcome messages from humpack's terminal. +- Fixed start and spectate buttons shrinking in the server lobby every time they're hidden and re-enabled. +- Fixed contained items inside contained items not moving when repositioning a container in the sub editor (e.g. when moving a weapon holder that contains a weapon with a magazine). +- Fixed issues with inaccurate tooltips and incorrectly blocked out order nodes in character-specific command interface. +- Fixed contained items' status effects appearing at the top-left corner of the container if the contained items are not visible (e.g. particle-emitting fuel rods would emit the particles from the top-left corner of the reactor instead of the center). +- Fixed hanging wires not getting selected when selecting the items they're connected to. +- Fixed "divide by zero" console error when scaling construction barrier. +- Fixed ability to wire item between two submarines as long as you stay inside the same sub. +- Fixed crew list background blocking mouse input (again). +- Fixed crashing when the majority of the players are controlling characters belonging to a non-player team while the sub is at the end of the level (e.g. if you're alone in the sub and take control of a monster with console commands). + +Modding: +- Option to configure minimum damage for OnDamage status effects that require a specific type of affliction (see the "vigor on damage" affliction for an usage example). + --------------------------------------------------------------------------------------------------------- v0.1500.0.0 --------------------------------------------------------------------------------------------------------- @@ -25,6 +61,7 @@ Additions and changes: - Lever state is visualized on its sprite. - Enabled NVidia Optimus on Windows. + Overhauled status monitor: - Improved visuals. - Indicates the locations of the crew's ID cards.