diff --git a/Barotrauma/BarotraumaClient/ClientCode.projitems b/Barotrauma/BarotraumaClient/ClientCode.projitems index a5cc964a4..ab5b8970a 100644 --- a/Barotrauma/BarotraumaClient/ClientCode.projitems +++ b/Barotrauma/BarotraumaClient/ClientCode.projitems @@ -18,14 +18,13 @@ - + - diff --git a/Barotrauma/BarotraumaClient/Source/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/Source/Characters/Health/CharacterHealth.cs index a43ee6069..914ec1ee8 100644 --- a/Barotrauma/BarotraumaClient/Source/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/Source/Characters/Health/CharacterHealth.cs @@ -1,7 +1,1400 @@ -namespace Barotrauma +using Barotrauma.Items.Components; +using Barotrauma.Networking; +using Lidgren.Network; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma { partial class CharacterHealth { + private static bool toggledThisFrame; + + private static Sprite damageOverlay; + + private static string[] strengthTexts; + + private GUIButton cprButton; + + private Point screenResolution; + + private float uiScale, inventoryScale; + + private Alignment alignment = Alignment.Left; + public Alignment Alignment + { + get { return alignment; } + set + { + if (alignment == value) return; + alignment = value; + UpdateAlignment(); + } + } + + private GUIButton suicideButton; + + // healthbars + private GUIProgressBar healthBar; + private GUIProgressBar healthBarShadow; + private GUIProgressBar healthWindowHealthBar; + private GUIProgressBar healthWindowHealthBarShadow; + private float healthShadowSize; + private float healthShadowDelay; + private float healthBarPulsateTimer; + private float healthBarPulsatePhase; + + private GUITextBlock characterName; + private GUIFrame afflictionInfoFrame; + private GUIListBox afflictionInfoContainer; + private GUIListBox recommendedTreatmentContainer; + + private float bloodParticleTimer; + + private GUIFrame healthWindow; + + private GUIComponent deadIndicator; + + private GUIComponent lowSkillIndicator; + + private SpriteSheet limbIndicatorOverlay; + private float limbIndicatorOverlayAnimState; + + private GUIFrame dropItemArea; + + private float dropItemAnimDuration = 0.5f; + private float dropItemAnimTimer; + private Item droppedItem; + + private GUIComponent draggingMed; + + private int highlightedLimbIndex = -1; + private int selectedLimbIndex = -1; + private LimbHealth currentDisplayedLimb; + + private float distortTimer; + + // 0-1 + private float damageIntensity; + private float damageIntensityDropdownRate = 0.1f; + + public float DamageOverlayTimer { get; private set; } + + private float updateDisplayedAfflictionsTimer; + private const float UpdateDisplayedAfflictionsInterval = 0.5f; + private List currentDisplayedAfflictions = new List(); + + private static CharacterHealth openHealthWindow; + public static CharacterHealth OpenHealthWindow + { + get + { + return openHealthWindow; + } + set + { + if (openHealthWindow == value) return; + if (value != null && !value.UseHealthWindow) return; + + openHealthWindow = value; + toggledThisFrame = true; + if (Character.Controlled == null) { return; } + + if (value == null && + Character.Controlled?.SelectedCharacter?.CharacterHealth != null && + Character.Controlled.SelectedCharacter.CharacterHealth == openHealthWindow && + !Character.Controlled.SelectedCharacter.CanInventoryBeAccessed) + { + Character.Controlled.DeselectCharacter(); + } + + Character.Controlled.ResetInteract = true; + if (openHealthWindow != null) + { + openHealthWindow.characterName.Text = value.Character.Name; + Character.Controlled.SelectedConstruction = null; + } + } + } + + static CharacterHealth() + { + damageOverlay = new Sprite("Content/UI/damageOverlay.png", Vector2.Zero); + } + + partial void InitProjSpecific(XElement element, Character character) + { + if (strengthTexts == null) + { + strengthTexts = new string[] + { + TextManager.Get("AfflictionStrengthLow"), + TextManager.Get("AfflictionStrengthMedium"), + TextManager.Get("AfflictionStrengthHigh") + }; + } + + character.OnAttacked += OnAttacked; + + bool horizontal = HUDLayoutSettings.HealthBarAreaLeft.Width > HUDLayoutSettings.HealthBarAreaLeft.Height; + healthBar = new GUIProgressBar(HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.HealthBarAreaLeft, GUI.Canvas), + barSize: 1.0f, color: Color.Green, style: horizontal ? "GUIProgressBar" : "GUIProgressBarVertical") + { + IsHorizontal = horizontal + }; + healthBarShadow = new GUIProgressBar(HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.HealthBarAreaLeft, GUI.Canvas), + barSize: 1.0f, color: Color.Green, style: horizontal ? "GUIProgressBar" : "GUIProgressBarVertical") + { + IsHorizontal = horizontal + }; + healthShadowSize = 1.0f; + + afflictionInfoFrame = new GUIFrame(new RectTransform(new Point(HUDLayoutSettings.HealthWindowAreaLeft.Width / 2, 200), GUI.Canvas)); + var paddedInfoFrame = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.9f), afflictionInfoFrame.RectTransform, Anchor.Center), style: null); + new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.08f), paddedInfoFrame.RectTransform), "", font: GUI.LargeFont) + { + UserData = "selectedlimbname" + }; + + afflictionInfoContainer = new GUIListBox(new RectTransform(new Vector2(0.7f, 0.85f), paddedInfoFrame.RectTransform, Anchor.BottomLeft)); + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.08f), paddedInfoFrame.RectTransform), TextManager.Get("SuitableTreatments"), textAlignment: Alignment.TopRight); + lowSkillIndicator = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.07f), paddedInfoFrame.RectTransform, Anchor.TopRight) { RelativeOffset = new Vector2(0.0f, 0.08f) }, + TextManager.Get("LowMedicalSkillWarning"), Color.Orange, textAlignment: Alignment.Center, font: GUI.SmallFont, wrap: true) + { + Visible = false + }; + recommendedTreatmentContainer = new GUIListBox(new RectTransform(new Vector2(0.28f, 0.5f), paddedInfoFrame.RectTransform, Anchor.TopRight) { RelativeOffset = new Vector2(0.0f, 0.15f) }) + { + Spacing = 10 + }; + dropItemArea = new GUIFrame(new RectTransform(new Vector2(0.28f, 0.3f), paddedInfoFrame.RectTransform, Anchor.BottomRight) + { RelativeOffset = new Vector2(0.0f, 0.0f) }, style: null) + { + ToolTip = TextManager.Get("HealthItemUseTip") + }; + dropItemArea.RectTransform.NonScaledSize = new Point(dropItemArea.Rect.Width); + + string[] healthCircleStyles = new string[] { "HealthCircleInner", "HealthCircleMid", "HealthCircleOuter" }; + foreach (string healthCircleStyle in healthCircleStyles) + { + for (int i = 1; i < 4; i++) + { + var style = GUI.Style.GetComponentStyle(healthCircleStyle + i); + if (style != null) + { + new GUIImage(new RectTransform(Vector2.One, dropItemArea.RectTransform), healthCircleStyle + i) + { + CanBeFocused = false + }; + } + } + } + + new GUIImage(new RectTransform(Vector2.One * 0.2f, dropItemArea.RectTransform, Anchor.Center), "HealthCross") + { + CanBeFocused = false + }; + + healthWindow = new GUIFrame(new RectTransform(new Point(100, 200), GUI.Canvas)); + var paddedHealthWindow = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), healthWindow.RectTransform, Anchor.Center)) + { + Stretch = true, + RelativeSpacing = 0.03f + }; + + var nameContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), paddedHealthWindow.RectTransform) { MinSize = new Point(0, 20) }, isHorizontal: true) + { + Stretch = true + }; + + characterName = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), nameContainer.RectTransform), "", textAlignment: Alignment.CenterLeft, font: GUI.LargeFont) + { + AutoScale = true + }; + new GUICustomComponent(new RectTransform(new Vector2(0.4f, 1.0f), nameContainer.RectTransform), + onDraw: (spriteBatch, component) => + { + character.Info.DrawPortrait(spriteBatch, new Vector2(component.Rect.X, component.Rect.Center.Y - component.Rect.Width / 2), component.Rect.Width); + }); + + new GUICustomComponent(new RectTransform(new Vector2(1.0f, 0.9f), paddedHealthWindow.RectTransform), + (spriteBatch, component) => + { + DrawHealthWindow(spriteBatch, component.RectTransform.Rect, true, false); + }, + (deltaTime, component) => + { + UpdateLimbIndicators(deltaTime, component.RectTransform.Rect); + } + ); + deadIndicator = new GUITextBlock(new RectTransform(new Vector2(0.9f, 0.1f), healthWindow.RectTransform, Anchor.Center), + text: TextManager.Get("Deceased"), font: GUI.LargeFont, textAlignment: Alignment.Center, wrap: true, style: "GUIToolTip") + { + Visible = false, + CanBeFocused = false + }; + + healthWindowHealthBar = new GUIProgressBar(HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.HealthBarAreaLeft, GUI.Canvas), + barSize: 1.0f, color: Color.Green, style: "GUIProgressBarVertical") + { + IsHorizontal = false + }; + healthWindowHealthBarShadow = new GUIProgressBar(HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.HealthBarAreaLeft, GUI.Canvas), + barSize: 1.0f, color: Color.Green, style: "GUIProgressBarVertical") + { + IsHorizontal = false + }; + cprButton = new GUIButton(new RectTransform(new Point(80, 80), GUI.Canvas), text: "", style: "CPRButton") + { + OnClicked = (button, userData) => + { + Character selectedCharacter = Character.Controlled?.SelectedCharacter; + if (selectedCharacter == null || (!selectedCharacter.IsUnconscious && selectedCharacter.Stun <= 0.0f)) return false; + + Character.Controlled.AnimController.Anim = (Character.Controlled.AnimController.Anim == AnimController.Animation.CPR) ? + AnimController.Animation.None : AnimController.Animation.CPR; + + selectedCharacter.AnimController.ResetPullJoints(); + + if (GameMain.Client != null) + { + GameMain.Client.CreateEntityEvent(Character.Controlled, new object[] { NetEntityEvent.Type.Treatment }); + } + + return true; + }, + Visible = false + }; + + UpdateAlignment(); + + suicideButton = new GUIButton(new RectTransform(new Vector2(0.06f, 0.02f), GUI.Canvas, Anchor.TopCenter) + { MinSize = new Point(120, 20), RelativeOffset = new Vector2(0.0f, 0.01f) }, + TextManager.Get("GiveInButton")) + { + ToolTip = TextManager.Get(GameMain.NetworkMember == null ? "GiveInHelpSingleplayer" : "GiveInHelpMultiplayer"), + OnClicked = (button, userData) => + { + GUI.ForceMouseOn(null); + if (Character.Controlled != null) + { + if (GameMain.Client != null) + { + GameMain.Client.CreateEntityEvent(Character.Controlled, new object[] { NetEntityEvent.Type.Status }); + } + else + { + var causeOfDeath = GetCauseOfDeath(); + Character.Controlled.Kill(causeOfDeath.First, causeOfDeath.Second); + Character.Controlled = null; + } + } + return true; + } + }; + + if (element != null) + { + foreach (XElement subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "sprite": + limbIndicatorOverlay = new SpriteSheet(subElement); + break; + } + } + } + } + + private void OnAttacked(Character attacker, AttackResult attackResult) + { + if (Math.Abs(attackResult.Damage) < 0.01f && attackResult.Afflictions.Count == 0) return; + DamageOverlayTimer = MathHelper.Clamp(attackResult.Damage / MaxVitality, DamageOverlayTimer, 1.0f); + if (healthShadowDelay <= 0.0f) healthShadowDelay = 1.0f; + + if (healthBarPulsateTimer <= 0.0f) healthBarPulsatePhase = 0.0f; + healthBarPulsateTimer = 1.0f; + + float additionalIntensity = MathHelper.Lerp(0, 1, MathUtils.InverseLerp(0, 0.1f, attackResult.Damage / MaxVitality)); + damageIntensity = MathHelper.Clamp(damageIntensity + additionalIntensity, 0, 1); + } + + private void UpdateAlignment() + { + healthBar.RectTransform.RelativeOffset = healthBarShadow.RectTransform.RelativeOffset = Vector2.Zero; + healthWindowHealthBar.RectTransform.RelativeOffset = healthWindowHealthBarShadow.RectTransform.RelativeOffset = Vector2.Zero; + + int healthWindowHealthBarWidth = (int)(40 * GUI.Scale); + + if (alignment == Alignment.Left) + { + healthBar.RectTransform.SetPosition(Anchor.BottomLeft); + healthBarShadow.RectTransform.SetPosition(Anchor.BottomLeft); + healthBar.RectTransform.AbsoluteOffset = healthBarShadow.RectTransform.AbsoluteOffset = + new Point(HUDLayoutSettings.HealthBarAreaLeft.X, GameMain.GraphicsHeight - HUDLayoutSettings.HealthBarAreaLeft.Bottom); + healthBar.RectTransform.NonScaledSize = healthBarShadow.RectTransform.NonScaledSize = HUDLayoutSettings.HealthBarAreaLeft.Size; + + healthWindow.RectTransform.AbsoluteOffset = HUDLayoutSettings.HealthWindowAreaLeft.Location + new Point(healthWindowHealthBarWidth, 0); + healthWindow.RectTransform.NonScaledSize = new Point( + HUDLayoutSettings.HealthWindowAreaLeft.Width / 3 - healthWindowHealthBarWidth, + HUDLayoutSettings.HealthWindowAreaLeft.Height); + + afflictionInfoFrame.RectTransform.AbsoluteOffset = new Point( + healthWindow.Rect.Right, + HUDLayoutSettings.HealthWindowAreaLeft.Y); + afflictionInfoFrame.RectTransform.NonScaledSize = new Point( + (int)(HUDLayoutSettings.HealthWindowAreaLeft.Width * 0.66f), + (int)(HUDLayoutSettings.HealthWindowAreaLeft.Height)); + + healthWindowHealthBar.RectTransform.NonScaledSize = healthWindowHealthBarShadow.RectTransform.NonScaledSize = + new Point(healthWindowHealthBarWidth, healthWindow.Rect.Height); + healthWindowHealthBar.RectTransform.AbsoluteOffset = healthWindowHealthBarShadow.RectTransform.AbsoluteOffset = + HUDLayoutSettings.HealthWindowAreaLeft.Location; + + int cprButtonSize = (int)(100 * GUI.Scale); + cprButton.RectTransform.AbsoluteOffset = new Point(HUDLayoutSettings.HealthWindowAreaLeft.Right, dropItemArea.Rect.Center.Y - cprButtonSize / 2); + cprButton.RectTransform.NonScaledSize = new Point(cprButtonSize); + } + else + { + healthBar.RectTransform.SetPosition(Anchor.TopLeft); + healthBarShadow.RectTransform.SetPosition(Anchor.TopLeft); + healthBar.RectTransform.AbsoluteOffset = healthBarShadow.RectTransform.AbsoluteOffset = + HUDLayoutSettings.HealthBarAreaRight.Location; + healthBar.RectTransform.NonScaledSize = healthBarShadow.RectTransform.NonScaledSize = HUDLayoutSettings.HealthBarAreaRight.Size; + + healthWindow.RectTransform.AbsoluteOffset = new Point( + HUDLayoutSettings.HealthWindowAreaRight.X + HUDLayoutSettings.HealthWindowAreaRight.Width / 3 * 2, + HUDLayoutSettings.HealthWindowAreaRight.Y); + healthWindow.RectTransform.NonScaledSize = new Point( + HUDLayoutSettings.HealthWindowAreaRight.Width / 3 - healthWindowHealthBarWidth, + HUDLayoutSettings.HealthWindowAreaRight.Height); + + afflictionInfoFrame.RectTransform.AbsoluteOffset = new Point( + HUDLayoutSettings.HealthWindowAreaRight.X, + HUDLayoutSettings.HealthWindowAreaLeft.Y); + afflictionInfoFrame.RectTransform.NonScaledSize = new Point( + (int)(HUDLayoutSettings.HealthWindowAreaLeft.Width * 0.66f), + (int)(HUDLayoutSettings.HealthWindowAreaLeft.Height)); + + healthWindowHealthBar.RectTransform.NonScaledSize = healthWindowHealthBarShadow.RectTransform.NonScaledSize = + new Point(healthWindowHealthBarWidth, healthWindow.Rect.Height); + healthWindowHealthBar.RectTransform.AbsoluteOffset = healthWindowHealthBarShadow.RectTransform.AbsoluteOffset = + new Point(HUDLayoutSettings.HealthWindowAreaRight.Right - healthWindowHealthBarWidth, HUDLayoutSettings.HealthWindowAreaRight.Y); + + int cprButtonSize = (int)(100 * GUI.Scale); + cprButton.RectTransform.AbsoluteOffset = new Point(HUDLayoutSettings.HealthWindowAreaRight.X - cprButtonSize, dropItemArea.Rect.Center.Y - cprButtonSize / 2); + cprButton.RectTransform.NonScaledSize = new Point(cprButtonSize); + } + + dropItemArea.RectTransform.NonScaledSize = new Point(dropItemArea.Rect.Width); + + screenResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); + inventoryScale = Inventory.UIScale; + uiScale = GUI.Scale; + } + + partial void UpdateOxygenProjSpecific(float prevOxygen) + { + if (prevOxygen > 0.0f && OxygenAmount <= 0.0f && + Character.Controlled == Character) + { + SoundPlayer.PlaySound(Character.Info != null && Character.Info.Gender == Gender.Female ? "drownfemale" : "drownmale"); + } + } + + partial void UpdateBleedingProjSpecific(AfflictionBleeding affliction, Limb targetLimb, float deltaTime) + { + bloodParticleTimer -= deltaTime * (affliction.Strength / 10.0f); + if (bloodParticleTimer <= 0.0f) + { + float bloodParticleSize = MathHelper.Lerp(0.5f, 1.0f, affliction.Strength / 100.0f); + if (!Character.AnimController.InWater) bloodParticleSize *= 2.0f; + var blood = GameMain.ParticleManager.CreateParticle( + Character.AnimController.InWater ? "waterblood" : "blooddrop", + targetLimb.WorldPosition, Rand.Vector(affliction.Strength), 0.0f, Character.AnimController.CurrentHull); + + if (blood != null) + { + blood.Size *= bloodParticleSize; + } + bloodParticleTimer = 1.0f; + } + } + + public void UpdateHUD(float deltaTime) + { + if (GUI.DisableHUD) return; + if (openHealthWindow != null) + { + if (openHealthWindow != Character.Controlled?.CharacterHealth && openHealthWindow != Character.Controlled?.SelectedCharacter?.CharacterHealth) + { + openHealthWindow = null; + return; + } + } + + bool forceAfflictionContainerUpdate = false; + if (updateDisplayedAfflictionsTimer > 0.0f) + { + updateDisplayedAfflictionsTimer -= deltaTime; + } + else + { + forceAfflictionContainerUpdate = true; + currentDisplayedAfflictions = GetAllAfflictions() + .Where(a => a.Strength >= a.Prefab.ShowIconThreshold && a.Prefab.Icon != null).ToList(); + currentDisplayedAfflictions.Sort((a1, a2) => + { + int dmgPerSecond = Math.Sign(a2.DamagePerSecond - a1.DamagePerSecond); + return dmgPerSecond != 0 ? dmgPerSecond : Math.Sign(a1.Strength - a1.Strength); + }); + updateDisplayedAfflictionsTimer = UpdateDisplayedAfflictionsInterval; + } + + if (DamageOverlayTimer > 0.0f) + { + DamageOverlayTimer -= deltaTime; + } + if (damageIntensity > 0) + { + damageIntensity -= deltaTime * damageIntensityDropdownRate; + if (damageIntensity < 0) + { + damageIntensity = 0; + } + } + + if (healthShadowDelay > 0.0f) + { + healthShadowDelay -= deltaTime; + } + else + { + healthShadowSize = healthBar.BarSize > healthShadowSize ? + Math.Min(healthShadowSize + deltaTime, healthBar.BarSize) : + Math.Max(healthShadowSize - deltaTime, healthBar.BarSize); + } + + dropItemArea.Visible = !Character.IsDead; + + float blurStrength = 0.0f; + float distortStrength = 0.0f; + float distortSpeed = 0.0f; + float radialDistortStrength = 0.0f; + float chromaticAberrationStrength = 0.0f; + + if (Character.IsUnconscious) + { + blurStrength = 1.0f; + distortSpeed = 1.0f; + } + else if (OxygenAmount < 100.0f) + { + blurStrength = MathHelper.Lerp(0.5f, 1.0f, 1.0f - Vitality / MaxVitality); + distortStrength = blurStrength; + distortSpeed = (blurStrength + 1.0f); + distortSpeed *= distortSpeed * distortSpeed * distortSpeed; + } + + foreach (Affliction affliction in afflictions) + { + distortStrength = Math.Max(distortStrength, affliction.GetScreenDistortStrength()); + blurStrength = Math.Max(blurStrength, affliction.GetScreenBlurStrength()); + radialDistortStrength = Math.Max(radialDistortStrength, affliction.GetRadialDistortStrength()); + chromaticAberrationStrength = Math.Max(chromaticAberrationStrength, affliction.GetChromaticAberrationStrength()); + } + foreach (LimbHealth limbHealth in limbHealths) + { + foreach (Affliction affliction in limbHealth.Afflictions) + { + distortStrength = Math.Max(distortStrength, affliction.GetScreenDistortStrength()); + blurStrength = Math.Max(blurStrength, affliction.GetScreenBlurStrength()); + radialDistortStrength = Math.Max(radialDistortStrength, affliction.GetRadialDistortStrength()); + chromaticAberrationStrength = Math.Max(chromaticAberrationStrength, affliction.GetChromaticAberrationStrength()); + } + } + + Character.RadialDistortStrength = radialDistortStrength; + Character.ChromaticAberrationStrength = chromaticAberrationStrength; + if (blurStrength > 0.0f) + { + distortTimer = (distortTimer + deltaTime * distortSpeed) % MathHelper.TwoPi; + Character.BlurStrength = (float)(Math.Sin(distortTimer) + 1.5f) * 0.25f * blurStrength; + Character.DistortStrength = (float)(Math.Sin(distortTimer) + 1.0f) * 0.1f * distortStrength; + } + else + { + Character.BlurStrength = 0.0f; + Character.DistortStrength = 0.0f; + distortTimer = 0.0f; + } + + if (PlayerInput.KeyHit(InputType.Health) && GUI.KeyboardDispatcher.Subscriber == null && + Character.AllowInput && Character.FocusedCharacter == null && !toggledThisFrame) + { + if (openHealthWindow != null) + OpenHealthWindow = null; + else + { + OpenHealthWindow = this; + forceAfflictionContainerUpdate = true; + } + } + else if (openHealthWindow == this) + { + if (Alignment == Alignment.Right ? + HUD.CloseHUD(HUDLayoutSettings.HealthWindowAreaRight) : + HUD.CloseHUD(HUDLayoutSettings.HealthWindowAreaLeft)) + { + //emulate a Health input to get the character to deselect the item server-side + Character.Keys[(int)InputType.Health].Hit = true; + OpenHealthWindow = null; + } + } + toggledThisFrame = false; + + if (OpenHealthWindow == this) + { + var highlightedLimb = highlightedLimbIndex < 0 ? null : limbHealths[highlightedLimbIndex]; + if (highlightedLimbIndex < 0 && selectedLimbIndex < 0) + { + // If no limb is selected or highlighted, select the one with the most critical afflictions. + var affliction = GetAllAfflictions(a => a.Prefab.IndicatorLimb != LimbType.None) + .OrderByDescending(a => a.DamagePerSecond) + .ThenByDescending(a => a.Strength).FirstOrDefault(); + var limbHealth = GetMathingLimbHealth(affliction); + if (limbHealth != null) + { + selectedLimbIndex = limbHealths.IndexOf(limbHealth); + } + } + LimbHealth selectedLimb = selectedLimbIndex < 0 ? highlightedLimb : limbHealths[selectedLimbIndex]; + if (selectedLimb != currentDisplayedLimb || forceAfflictionContainerUpdate) + { + UpdateAfflictionContainer(selectedLimb); + currentDisplayedLimb = selectedLimb; + } + } + + if (Character.IsDead) + { + healthBar.Color = healthWindowHealthBar.Color = Color.Black; + healthBar.BarSize = healthWindowHealthBar.BarSize = 1.0f; + } + else + { + healthBar.Color = healthWindowHealthBar.Color = ToolBox.GradientLerp(Vitality / MaxVitality, Color.Red, Color.Orange, Color.Green); + healthBar.HoverColor = healthWindowHealthBar.HoverColor = healthBar.Color * 2.0f; + healthBar.BarSize = healthWindowHealthBar.BarSize = + (Vitality > 0.0f) ? + (MaxVitality > 0.0f ? Vitality / MaxVitality : 0.0f) : + (Math.Abs(MinVitality) > 0.0f ? 1.0f - Vitality / MinVitality : 0.0f); + + if (healthBarPulsateTimer > 0.0f) + { + //0-1 + float pulsateAmount = (float)(Math.Sin(healthBarPulsatePhase) + 1.0f) / 2.0f; + + healthBar.RectTransform.LocalScale = healthBarShadow.RectTransform.LocalScale = new Vector2(1.0f, (1.0f + pulsateAmount * healthBarPulsateTimer * 0.5f)); + healthBarPulsatePhase += deltaTime * 5.0f; + healthBarPulsateTimer -= deltaTime; + } + else + { + healthBar.RectTransform.LocalScale = Vector2.One; + } + } + + if (OpenHealthWindow == this) + { + if (Character == Character.Controlled && !Character.AllowInput) + { + openHealthWindow = null; + } + + lowSkillIndicator.Visible = Timing.TotalTime % 1.0f < 0.8f && Character.Controlled != null && Character.Controlled.GetSkillLevel("medical") < 50.0f; + + float rotationSpeed = 0.25f; + int i = 0; + foreach (GUIComponent dropItemIndicator in dropItemArea.Children) + { + GUIImage img = dropItemIndicator as GUIImage; + if (img == null) continue; + + img.State = GUI.MouseOn == dropItemArea ? GUIComponent.ComponentState.Hover : GUIComponent.ComponentState.None; + + byte alpha = img.Color.A; + byte hoverAlpha = img.HoverColor.A; + img.Color = ToolBox.GradientLerp(Vitality / MaxVitality, Color.Red, Color.Orange, Color.Green); + img.Color = new Color(img.Color.R, img.Color.G, img.Color.B, alpha); + img.HoverColor = new Color(img.Color.R, img.Color.G, img.Color.B, hoverAlpha); + img.HoverColor = Color.Lerp(img.HoverColor, Color.White, 0.5f); + + if (img.State == GUIComponent.ComponentState.Hover && droppedItem == null) + { + dropItemAnimTimer = Math.Min(0.3f, dropItemAnimTimer + deltaTime * 0.5f); + } + + if (i < 4) + { + img.Scale = 1.0f - (float)Math.Sin(dropItemAnimTimer / dropItemAnimDuration * MathHelper.TwoPi) * 0.3f; + } + + if (dropItemIndicator == dropItemArea.Children.Last()) break; + img.Rotation = (img.Rotation + (rotationSpeed + dropItemAnimTimer * 10.0f) * deltaTime) % MathHelper.TwoPi; + rotationSpeed = (rotationSpeed + 0.3f) % 1.0f; + + i++; + } + + if (Inventory.draggingItem != null) + { + if (highlightedLimbIndex > -1) + { + selectedLimbIndex = highlightedLimbIndex; + } + } + + if (draggingMed != null) + { + if (!PlayerInput.LeftButtonHeld()) + { + OnItemDropped(draggingMed.UserData as Item, ignoreMousePos: false); + draggingMed = null; + } + } + + /*if (GUI.MouseOn?.UserData is Affliction affliction) + { + ShowAfflictionInfo(affliction, afflictionInfoContainer); + }*/ + + if (dropItemAnimTimer > 0.0f) + { + dropItemAnimTimer -= deltaTime; + if (dropItemAnimTimer <= 0.0f) droppedItem = null; + } + } + else + { + if (openHealthWindow != null && Character != Character.Controlled && Character != Character.Controlled?.SelectedCharacter) + { + openHealthWindow = null; + } + highlightedLimbIndex = -1; + } + + Rectangle hoverArea = alignment == Alignment.Left ? + Rectangle.Union(HUDLayoutSettings.AfflictionAreaLeft, HUDLayoutSettings.HealthBarAreaLeft) : + Rectangle.Union(HUDLayoutSettings.AfflictionAreaRight, HUDLayoutSettings.HealthBarAreaRight); + + if (Character.AllowInput && UseHealthWindow && hoverArea.Contains(PlayerInput.MousePosition) && Inventory.SelectedSlot == null) + { + healthBar.State = GUIComponent.ComponentState.Hover; + if (PlayerInput.LeftButtonClicked()) + { + OpenHealthWindow = openHealthWindow == this ? null : this; + } + } + else + { + healthBar.State = GUIComponent.ComponentState.None; + } + + suicideButton.Visible = Character == Character.Controlled && Character.IsUnconscious && !Character.IsDead; + + cprButton.Visible = + Character == Character.Controlled?.SelectedCharacter + && (Character.IsUnconscious || Character.Stun > 0.0f) + && !Character.IsDead + && openHealthWindow == this; + + deadIndicator.Visible = Character.IsDead; + } + + public void AddToGUIUpdateList() + { + if (GUI.DisableHUD) return; + if (OpenHealthWindow == this) + { + //afflictionContainer.AddToGUIUpdateList(); + afflictionInfoFrame.AddToGUIUpdateList(); + healthWindow.AddToGUIUpdateList(); + healthWindowHealthBarShadow.AddToGUIUpdateList(); + healthWindowHealthBar.AddToGUIUpdateList(); + } + else if (Character.Controlled == Character) + { + healthBarShadow.AddToGUIUpdateList(); + healthBar.AddToGUIUpdateList(); + } + if (suicideButton.Visible && Character == Character.Controlled) suicideButton.AddToGUIUpdateList(); + if (cprButton != null && cprButton.Visible) cprButton.AddToGUIUpdateList(); + } + + public void DrawHUD(SpriteBatch spriteBatch) + { + if (GUI.DisableHUD) return; + if (GameMain.GraphicsWidth != screenResolution.X || + GameMain.GraphicsHeight != screenResolution.Y || + Math.Abs(inventoryScale - Inventory.UIScale) > 0.01f || + Math.Abs(uiScale - GUI.Scale) > 0.01f) + { + UpdateAlignment(); + } + + float damageOverlayAlpha = DamageOverlayTimer; + if (Vitality < MaxVitality * 0.1f) + { + damageOverlayAlpha = Math.Max(1.0f - (Vitality / maxVitality * 10.0f), damageOverlayAlpha); + } + else + { + float pulsateAmount = (float)(Math.Sin(healthBarPulsatePhase) + 1.0f) / 2.0f; + damageOverlayAlpha = pulsateAmount * healthBarPulsateTimer * damageIntensity; + } + + if (damageOverlayAlpha > 0.0f) + { + damageOverlay.Draw(spriteBatch, Vector2.Zero, Color.White * damageOverlayAlpha, Vector2.Zero, 0.0f, + new Vector2(GameMain.GraphicsWidth / damageOverlay.size.X, GameMain.GraphicsHeight / damageOverlay.size.Y)); + } + + if (Character.Inventory != null) + { + if (Character.Inventory.CurrentLayout == CharacterInventory.Layout.Right) + { + //move the healthbar on top of the inventory slots + healthBar.RectTransform.ScreenSpaceOffset = new Point( + (GameMain.GraphicsWidth - HUDLayoutSettings.Padding) - HUDLayoutSettings.HealthBarAreaRight.Right, + HUDLayoutSettings.HealthBarAreaRight.Y - (int)(Character.Inventory.SlotPositions.Max(s => s.Y) + Inventory.EquipIndicator.size.Y * Inventory.UIScale * 2) - HUDLayoutSettings.HealthBarAreaRight.Height); + healthBarShadow.RectTransform.ScreenSpaceOffset = healthBar.RectTransform.ScreenSpaceOffset; + } + else + { + healthBar.RectTransform.ScreenSpaceOffset = healthBarShadow.RectTransform.ScreenSpaceOffset = Point.Zero; + } + } + + DrawStatusHUD(spriteBatch); + } + + public void DrawStatusHUD(SpriteBatch spriteBatch) + { + //Rectangle interactArea = healthBar.Rect; + if (openHealthWindow != this) + { + List> statusIcons = new List>(); + if (Character.CurrentHull == null || Character.CurrentHull.LethalPressure > 5.0f) + statusIcons.Add(new Pair(pressureAffliction, TextManager.Get("PressureHUDWarning"))); + if (Character.CurrentHull != null && Character.OxygenAvailable < LowOxygenThreshold && oxygenLowAffliction.Strength < oxygenLowAffliction.Prefab.ShowIconThreshold) + statusIcons.Add(new Pair(oxygenLowAffliction, TextManager.Get("OxygenHUDWarning"))); + + foreach (Affliction affliction in currentDisplayedAfflictions) + { + statusIcons.Add(new Pair(affliction, affliction.Prefab.Name)); + } + + Pair highlightedIcon = null; + Vector2 highlightedIconPos = Vector2.Zero; + Rectangle afflictionArea = alignment == Alignment.Left ? HUDLayoutSettings.AfflictionAreaLeft : HUDLayoutSettings.AfflictionAreaRight; + Point pos = afflictionArea.Location + healthBar.RectTransform.ScreenSpaceOffset; + + bool horizontal = afflictionArea.Width > afflictionArea.Height; + int iconSize = horizontal ? afflictionArea.Height : afflictionArea.Width; + + foreach (Pair statusIcon in statusIcons) + { + Rectangle afflictionIconRect = new Rectangle(pos, new Point(iconSize)); + if (afflictionIconRect.Contains(PlayerInput.MousePosition)) + { + highlightedIcon = statusIcon; + highlightedIconPos = afflictionIconRect.Center.ToVector2(); + } + + if (statusIcon.First.DamagePerSecond > 1.0f) + { + Rectangle glowRect = afflictionIconRect; + glowRect.Inflate((int)(25 * GUI.Scale), (int)(25 * GUI.Scale)); + var glow = GUI.Style.GetComponentStyle("OuterGlow"); + glow.Sprites[GUIComponent.ComponentState.None][0].Draw( + spriteBatch, glowRect, + Color.Red * (float)((Math.Sin(statusIcon.First.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); + + + statusIcon.First.Prefab.Icon?.Draw(spriteBatch, + pos.ToVector2(), + highlightedIcon == statusIcon ? statusIcon.First.Prefab.IconColor : statusIcon.First.Prefab.IconColor * 0.8f, + rotate: 0, + scale: iconSize / statusIcon.First.Prefab.Icon.size.X); + + if (horizontal) + pos.X += iconSize + (int)(5 * GUI.Scale); + else + pos.Y += iconSize + (int)(5 * GUI.Scale); + } + + if (highlightedIcon != null) + { + GUI.DrawString(spriteBatch, + alignment == Alignment.Left ? highlightedIconPos + new Vector2(60 * GUI.Scale, 5) : highlightedIconPos + new Vector2(-10.0f - GUI.Font.MeasureString(highlightedIcon.Second).X, 5), + highlightedIcon.Second, + Color.White * 0.8f, Color.Black * 0.5f); + } + + if (Vitality > 0.0f) + { + float currHealth = healthBar.BarSize; + Color prevColor = healthBar.Color; + healthBarShadow.BarSize = healthShadowSize; + healthBarShadow.Color = Color.Red; + healthBarShadow.Visible = true; + healthBar.BarSize = currHealth; + healthBar.Color = prevColor; + } + else + { + healthBarShadow.Visible = false; + } + } + else + { + if (Vitality > 0.0f) + { + float currHealth = healthWindowHealthBar.BarSize; + Color prevColor = healthWindowHealthBar.Color; + healthWindowHealthBarShadow.BarSize = healthShadowSize; + healthWindowHealthBarShadow.Color = Color.Red; + healthWindowHealthBarShadow.Visible = true; + healthWindowHealthBar.BarSize = currHealth; + healthWindowHealthBar.Color = prevColor; + } + else + { + healthWindowHealthBarShadow.Visible = false; + } + } + } + + private void UpdateAfflictionContainer(LimbHealth selectedLimb) + { + ((GUITextBlock)afflictionInfoContainer.Parent.GetChildByUserData("selectedlimbname")).Text = selectedLimb == null ? "" : selectedLimb.Name; + + if (selectedLimb == null) + { + afflictionInfoContainer.Content.ClearChildren(); + return; + } + var currentAfflictions = GetMatchingAfflictions(selectedLimb, a => a.Strength >= a.Prefab.ShowIconThreshold); + var displayedAfflictions = afflictionInfoContainer.Content.Children.Select(c => c.UserData as Affliction); + if (currentAfflictions.Any(a => !displayedAfflictions.Contains(a)) || + displayedAfflictions.Any(a => !currentAfflictions.Contains(a))) + { + CreateAfflictionInfos(currentAfflictions); + } + + UpdateAfflictionInfos(displayedAfflictions); + } + + private void CreateAfflictionInfos(IEnumerable afflictions) + { + afflictionInfoContainer.Content.ClearChildren(); + 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(); + float minSuitability = -10, maxSuitability = 10; + foreach (Affliction affliction in afflictions) + { + foreach (KeyValuePair treatment in affliction.Prefab.TreatmentSuitability) + { + if (!treatmentSuitability.ContainsKey(treatment.Key)) + { + treatmentSuitability[treatment.Key] = treatment.Value * affliction.Strength; + } + else + { + treatmentSuitability[treatment.Key] += treatment.Value * affliction.Strength; + } + minSuitability = Math.Min(treatmentSuitability[treatment.Key], minSuitability); + maxSuitability = Math.Max(treatmentSuitability[treatment.Key], maxSuitability); + } + } + //normalize the suitabilities to a range of 0 to 1 + foreach (string treatment in treatmentSuitability.Keys.ToList()) + { + treatmentSuitability[treatment] = (treatmentSuitability[treatment] - minSuitability) / (maxSuitability - minSuitability); + //lerp towards a random value if the medical skill is low + treatmentSuitability[treatment] = MathHelper.Lerp(treatmentSuitability[treatment], Rand.Range(0.0f, 1.0f), randomVariance); + } + + foreach (Affliction affliction in afflictions) + { + var child = new GUILayoutGroup(new RectTransform(Vector2.One * 0.95f, afflictionInfoContainer.Content.RectTransform, Anchor.TopCenter)) + { + Stretch = true, + RelativeSpacing = 0.02f, + UserData = affliction + }; + + var headerContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), child.RectTransform), isHorizontal: true) + { + Stretch = true, + UserData = "header" + }; + + new GUIImage(new RectTransform(new Vector2(0.15f, 1.0f), headerContainer.RectTransform), affliction.Prefab.Icon, scaleToFit: true) + { + Color = affliction.Prefab.IconColor + }; + + var labelContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 1.0f), headerContainer.RectTransform), isHorizontal: true) + { + Stretch = true, + AbsoluteSpacing = 10, + UserData = "label" + }; + var afflictionName = new GUITextBlock(new RectTransform(new Vector2(0.65f, 1.0f), labelContainer.RectTransform), affliction.Prefab.Name, textAlignment: Alignment.CenterLeft, font: GUI.LargeFont); + var afflictionStrength = new GUITextBlock(new RectTransform(new Vector2(0.35f, 0.6f), labelContainer.RectTransform), "", textAlignment: Alignment.TopRight, font: GUI.LargeFont) + { + Padding = Vector4.Zero, + UserData = "strength" + }; + var vitality = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), labelContainer.RectTransform, Anchor.BottomRight), "", textAlignment: Alignment.BottomRight) + { + IgnoreLayoutGroups = true, + UserData = "vitality" + }; + + var description = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), child.RectTransform), + affliction.Prefab.Description, textAlignment: Alignment.TopLeft, wrap: true); + if (description.Font.MeasureString(description.WrappedText).Y > description.Rect.Height) + { + description.Font = GUI.SmallFont; + } + description.RectTransform.Resize(new Point(description.Rect.Width, (int)(description.TextSize.Y + 10))); + child.RectTransform.Resize(new Point(child.Rect.Width, child.Children.Sum(c => c.Rect.Height))); + child.Recalculate(); + headerContainer.Recalculate(); + labelContainer.Recalculate(); + afflictionStrength.AutoScale = true; + afflictionName.AutoScale = true; + vitality.AutoDraw = true; + } + + List> treatmentSuitabilities = treatmentSuitability.OrderByDescending(t => t.Value).ToList(); + + foreach (KeyValuePair treatment in treatmentSuitabilities) + { + ItemPrefab item = MapEntityPrefab.Find(name: null, identifier: treatment.Key, showErrorMessages: false) as ItemPrefab; + if (item == null) continue; + int slotSize = (int)(recommendedTreatmentContainer.Content.Rect.Width * 0.8f); + + var itemSlot = new GUIButton(new RectTransform(new Point(slotSize), recommendedTreatmentContainer.Content.RectTransform, Anchor.TopCenter), + text: "", style: "InventorySlotSmall") + { + UserData = item + }; + itemSlot.Color = ToolBox.GradientLerp(treatment.Value, Color.Red, Color.White, Color.LightGreen); + + Sprite itemSprite = item.InventoryIcon ?? item.sprite; + Color itemColor = itemSprite == item.sprite ? item.SpriteColor : item.InventoryIconColor; + var itemIcon = new GUIImage(new RectTransform(new Vector2(0.8f, 0.8f), itemSlot.RectTransform, Anchor.Center), + itemSprite, scaleToFit: true) + { + CanBeFocused = false, + Color = itemColor, + HoverColor = itemColor, + SelectedColor = itemColor + }; + itemSlot.ToolTip = item.Name + "\n" + item.Description; + } + + afflictionInfoContainer.Content.RectTransform.SortChildren((r1, r2) => + { + var first = r1.GUIComponent.UserData as Affliction; + var second = r2.GUIComponent.UserData as Affliction; + int dmgPerSecond = Math.Sign(second.DamagePerSecond - first.DamagePerSecond); + return dmgPerSecond != 0 ? dmgPerSecond : Math.Sign(second.Strength - first.Strength); + }); + + //afflictionInfoContainer.Content.RectTransform.SortChildren((r1, r2) => + //{ + // return Math.Sign(((Affliction)r2.GUIComponent.UserData).GetVitalityDecrease(this) - ((Affliction)r1.GUIComponent.UserData).GetVitalityDecrease(this)); + //}); + } + + private void UpdateAfflictionInfos(IEnumerable afflictions) + { + foreach (Affliction affliction in afflictions) + { + var child = afflictionInfoContainer.Content.FindChild(affliction); + var headerContainer = child.GetChildByUserData("header"); + var labelContainer = headerContainer.GetChildByUserData("label"); + var strengthText = labelContainer.GetChildByUserData("strength") as GUITextBlock; + + strengthText.Text = strengthTexts[ + MathHelper.Clamp((int)Math.Floor((affliction.Strength / affliction.Prefab.MaxStrength) * strengthTexts.Length), 0, strengthTexts.Length - 1)]; + + strengthText.TextColor = ToolBox.GradientLerp( + affliction.Strength / affliction.Prefab.MaxStrength, + Color.Yellow, Color.Orange, Color.Red); + + var vitalityText = labelContainer.GetChildByUserData("vitality") as GUITextBlock; + int vitalityDecrease = (int)affliction.GetVitalityDecrease(this); + if (vitalityDecrease == 0) + { + vitalityText.Visible = false; + } + else + { + vitalityText.Visible = true; + vitalityText.Text = TextManager.Get("Vitality") + " -" + vitalityDecrease; + vitalityText.TextColor = vitalityDecrease <= 0 ? Color.LightGreen : + Color.Lerp(Color.Orange, Color.Red, affliction.Strength / affliction.Prefab.MaxStrength); + } + } + } + + public bool OnItemDropped(Item item, bool ignoreMousePos) + { + //items can be dropped outside the health window + if (!ignoreMousePos && + !healthWindow.Rect.Contains(PlayerInput.MousePosition) && + !afflictionInfoFrame.Rect.Contains(PlayerInput.MousePosition)) + { + return false; + } + + //can't apply treatment to dead characters + if (Character.IsDead) return true; + if (item == null || !item.UseInHealthInterface) return true; + if (!ignoreMousePos) + { + if (highlightedLimbIndex > -1) + { + selectedLimbIndex = highlightedLimbIndex; + } + else if (!dropItemArea.Rect.Contains(PlayerInput.MousePosition)) + { + return true; + } + } + + Limb targetLimb = Character.AnimController.Limbs.FirstOrDefault(l => l.HealthIndex == selectedLimbIndex); + + item.ApplyTreatment(Character.Controlled, Character, targetLimb); + + dropItemAnimTimer = dropItemAnimDuration; + droppedItem = item; + return true; + } + + private List GetAvailableMedicalItems() + { + List allInventoryItems = new List(); + allInventoryItems.AddRange(Character.Inventory.Items); + if (Character.SelectedCharacter?.Inventory != null && Character.CanAccessInventory(Character.SelectedCharacter.Inventory)) + { + allInventoryItems.AddRange(Character.SelectedCharacter.Inventory.Items); + } + if (Character.SelectedBy?.Inventory != null) + { + allInventoryItems.AddRange(Character.SelectedBy.Inventory.Items); + } + + List medicalItems = new List(); + foreach (Item item in allInventoryItems) + { + if (item == null) continue; + + var containedItems = item.ContainedItems; + if (containedItems != null) + { + foreach (Item containedItem in containedItems) + { + if (containedItem == null) continue; + 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) + { + limbIndicatorOverlayAnimState += deltaTime * 8.0f; + + highlightedLimbIndex = -1; + int i = 0; + foreach (LimbHealth limbHealth in limbHealths) + { + if (limbHealth.IndicatorSprite == null) continue; + + float scale = Math.Min(drawArea.Width / (float)limbHealth.IndicatorSprite.SourceRect.Width, drawArea.Height / (float)limbHealth.IndicatorSprite.SourceRect.Height); + + Rectangle highlightArea = GetLimbHighlightArea(limbHealth, drawArea); + + if (highlightArea.Contains(PlayerInput.MousePosition)) + { + highlightedLimbIndex = i; + } + i++; + } + + if (PlayerInput.LeftButtonClicked() && highlightedLimbIndex > -1) + { + selectedLimbIndex = highlightedLimbIndex; + //afflictionContainer.ClearChildren(); + afflictionInfoContainer.ClearChildren(); + } + } + + private void DrawHealthWindow(SpriteBatch spriteBatch, Rectangle drawArea, bool allowHighlight, bool highlightAll) + { + if (Character.Removed) { return; } + + int i = 0; + foreach (LimbHealth limbHealth in limbHealths) + { + if (limbHealth.IndicatorSprite == null) continue; + + float damageLerp = limbHealth.TotalDamage > 0.0f ? MathHelper.Lerp(0.2f, 1.0f, limbHealth.TotalDamage / 100.0f) : 0.0f; + Color color = Character.IsDead ? + Color.Lerp(Color.Black, new Color(150, 100, 100), damageLerp) : + ToolBox.GradientLerp(damageLerp, Color.Green, Color.Orange, Color.Red); + float scale = Math.Min(drawArea.Width / (float)limbHealth.IndicatorSprite.SourceRect.Width, drawArea.Height / (float)limbHealth.IndicatorSprite.SourceRect.Height); + + if (((i == highlightedLimbIndex || i == selectedLimbIndex) && allowHighlight) || highlightAll) + { + color = Color.Lerp(color, Color.White, 0.5f); + } + + limbHealth.IndicatorSprite.Draw(spriteBatch, + drawArea.Center.ToVector2(), color, + limbHealth.IndicatorSprite.Origin, + 0, scale); + i++; + } + + spriteBatch.End(); + spriteBatch.Begin(SpriteSortMode.Deferred, Lights.CustomBlendStates.Multiplicative); + + float overlayScale = Math.Min( + drawArea.Width / (float)limbIndicatorOverlay.FrameSize.X, + drawArea.Height / (float)limbIndicatorOverlay.FrameSize.Y); + + int frame = 0; + int frameCount = 17; + if (limbIndicatorOverlayAnimState >= frameCount * 2) limbIndicatorOverlayAnimState = 0.0f; + if (limbIndicatorOverlayAnimState < frameCount) + { + frame = (int)limbIndicatorOverlayAnimState; + } + else + { + frame = frameCount - (int)(limbIndicatorOverlayAnimState - (frameCount - 1)); + } + + limbIndicatorOverlay.Draw(spriteBatch, frame, drawArea.Center.ToVector2(), Color.Gray, origin: limbIndicatorOverlay.FrameSize.ToVector2() / 2, rotate: 0.0f, + scale: Vector2.One * overlayScale); + + spriteBatch.End(); + spriteBatch.Begin(SpriteSortMode.Deferred, blendState: BlendState.AlphaBlend, rasterizerState: GameMain.ScissorTestEnable); + + i = 0; + foreach (LimbHealth limbHealth in limbHealths) + { + if (limbHealth.IndicatorSprite == null) continue; + float scale = Math.Min(drawArea.Width / (float)limbHealth.IndicatorSprite.SourceRect.Width, drawArea.Height / (float)limbHealth.IndicatorSprite.SourceRect.Height); + + Rectangle highlightArea = new Rectangle( + (int)(drawArea.Center.X - (limbHealth.IndicatorSprite.Texture.Width / 2 - limbHealth.HighlightArea.X) * scale), + (int)(drawArea.Center.Y - (limbHealth.IndicatorSprite.Texture.Height / 2 - limbHealth.HighlightArea.Y) * scale), + (int)(limbHealth.HighlightArea.Width * scale), + (int)(limbHealth.HighlightArea.Height * scale)); + + if (selectedLimbIndex == i) + { + if (alignment == Alignment.Left) + { + GUI.DrawLine(spriteBatch, + highlightArea.Center.ToVector2(), + afflictionInfoContainer.Parent.Rect.Location.ToVector2() + Vector2.UnitY * 20, + Color.LightBlue * 0.3f, 0, 4); + } + else + { + GUI.DrawLine(spriteBatch, + highlightArea.Center.ToVector2(), + new Vector2(afflictionInfoContainer.Parent.Rect.Right, afflictionInfoContainer.Parent.Rect.Y + 20), + Color.LightBlue * 0.3f, 0, 4); + } + } + + var slot = GUI.Style.GetComponentStyle("AfflictionIconSlot"); + + float iconScale = 0.3f * scale; + Vector2 iconPos = highlightArea.Center.ToVector2(); + foreach (Affliction affliction in limbHealth.Afflictions) + { + DrawLimbAfflictionIcon(spriteBatch, affliction, slot, iconScale, ref iconPos); + } + + foreach (Affliction affliction in afflictions) + { + Limb indicatorLimb = Character.AnimController.GetLimb(affliction.Prefab.IndicatorLimb); + if (indicatorLimb != null && indicatorLimb.HealthIndex == i) + { + DrawLimbAfflictionIcon(spriteBatch, affliction, slot, iconScale, ref iconPos); + } + } + i++; + } + + if (draggingMed != null) + { + GUIImage itemImage = draggingMed.GetChild(); + float scale = Math.Min(40.0f / itemImage.Sprite.size.X, 40.0f / itemImage.Sprite.size.Y); + itemImage.Sprite.Draw(spriteBatch, PlayerInput.MousePosition, itemImage.Color, 0, scale); + } + + if (dropItemAnimTimer > 0.0f && droppedItem?.Prefab.InventoryIcon != null) + { + var droppedItemSprite = droppedItem.Prefab.InventoryIcon ?? droppedItem.Sprite; + droppedItemSprite.Draw(spriteBatch, dropItemArea.Rect.Center.ToVector2(), + droppedItemSprite == droppedItem.Sprite ? droppedItem.GetSpriteColor() : droppedItem.GetInventoryIconColor(), + origin: droppedItemSprite.size / 2, + scale: MathHelper.SmoothStep(0.0f, 100.0f / droppedItemSprite.size.Length(), dropItemAnimTimer / dropItemAnimDuration)); + } + } + + private void DrawLimbAfflictionIcon(SpriteBatch spriteBatch, Affliction affliction, GUIComponentStyle slotStyle, float iconScale, ref Vector2 iconPos) + { + if (affliction.Strength < affliction.Prefab.ShowIconThreshold) return; + Vector2 iconSize = (affliction.Prefab.Icon.size * iconScale); + + //afflictions that have a strength of less than 10 are faded out slightly + float alpha = MathHelper.Lerp(0.3f, 1.0f, + (affliction.Strength - affliction.Prefab.ShowIconThreshold) / Math.Min(affliction.Prefab.MaxStrength - affliction.Prefab.ShowIconThreshold, 10.0f)); + + slotStyle.Sprites[GUIComponent.ComponentState.None][0].Draw( + spriteBatch, + new Rectangle((iconPos - iconSize / 2.0f).ToPoint(), iconSize.ToPoint()), + slotStyle.Color * alpha); + affliction.Prefab.Icon.Draw(spriteBatch, iconPos - iconSize / 2.0f, affliction.Prefab.IconColor * alpha, 0, iconScale); + iconPos += new Vector2(10.0f, 20.0f) * iconScale; + } + + private Rectangle GetLimbHighlightArea(LimbHealth limbHealth, Rectangle drawArea) + { + float scale = Math.Min(drawArea.Width / (float)limbHealth.IndicatorSprite.SourceRect.Width, drawArea.Height / (float)limbHealth.IndicatorSprite.SourceRect.Height); + return new Rectangle( + (int)(drawArea.Center.X - (limbHealth.IndicatorSprite.Texture.Width / 2 - limbHealth.HighlightArea.X) * scale), + (int)(drawArea.Center.Y - (limbHealth.IndicatorSprite.Texture.Height / 2 - limbHealth.HighlightArea.Y) * scale), + (int)(limbHealth.HighlightArea.Width * scale), + (int)(limbHealth.HighlightArea.Height * scale)); + } + + public void ClientRead(NetBuffer inc) + { + List> newAfflictions = new List>(); + + byte afflictionCount = inc.ReadByte(); + for (int i = 0; i < afflictionCount; i++) + { + AfflictionPrefab afflictionPrefab = AfflictionPrefab.List[inc.ReadRangedInteger(0, AfflictionPrefab.List.Count - 1)]; + float afflictionStrength = inc.ReadRangedSingle(0.0f, afflictionPrefab.MaxStrength, 8); + + newAfflictions.Add(new Pair(afflictionPrefab, afflictionStrength)); + } + + foreach (Affliction affliction in afflictions) + { + //deactivate afflictions that weren't included in the network message + if (!newAfflictions.Any(a => a.First == affliction.Prefab)) + { + affliction.Strength = 0.0f; + } + } + + foreach (Pair newAffliction in newAfflictions) + { + Affliction existingAffliction = afflictions.Find(a => a.Prefab == newAffliction.First); + if (existingAffliction == null) + { + afflictions.Add(newAffliction.First.Instantiate(newAffliction.Second)); + } + else + { + existingAffliction.Strength = newAffliction.Second; + if (existingAffliction == stunAffliction) Character.SetStun(existingAffliction.Strength, true, true); + } + } + + List> newLimbAfflictions = new List>(); + byte limbAfflictionCount = inc.ReadByte(); + for (int i = 0; i < limbAfflictionCount; i++) + { + int limbIndex = inc.ReadRangedInteger(0, limbHealths.Count - 1); + AfflictionPrefab afflictionPrefab = AfflictionPrefab.List[inc.ReadRangedInteger(0, AfflictionPrefab.List.Count - 1)]; + float afflictionStrength = inc.ReadRangedSingle(0.0f, afflictionPrefab.MaxStrength, 8); + + newLimbAfflictions.Add(new Triplet(limbHealths[limbIndex], afflictionPrefab, afflictionStrength)); + } + + foreach (LimbHealth limbHealth in limbHealths) + { + foreach (Affliction affliction in limbHealth.Afflictions) + { + //deactivate afflictions that weren't included in the network message + if (!newLimbAfflictions.Any(a => a.First == limbHealth && a.Second == affliction.Prefab)) + { + affliction.Strength = 0.0f; + } + } + + foreach (Triplet newAffliction in newLimbAfflictions) + { + if (newAffliction.First != limbHealth) continue; + Affliction existingAffliction = limbHealth.Afflictions.Find(a => a.Prefab == newAffliction.Second); + if (existingAffliction == null) + { + limbHealth.Afflictions.Add(newAffliction.Second.Instantiate(newAffliction.Third)); + } + else + { + existingAffliction.Strength = newAffliction.Third; + } + } + } + } + partial void UpdateLimbAfflictionOverlays() { foreach (Limb limb in Character.AnimController.Limbs) @@ -18,5 +1411,20 @@ limb.DamageOverlayStrength /= limbHealths[limb.HealthIndex].Afflictions.Count; } } + + partial void RemoveProjSpecific() + { + foreach (LimbHealth limbHealth in limbHealths) + { + if (limbHealth.IndicatorSprite != null) + { + limbHealth.IndicatorSprite.Remove(); + limbHealth.IndicatorSprite = null; + } + } + + limbIndicatorOverlay?.Remove(); + limbIndicatorOverlay = null; + } } } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/Source/Characters/Health/Afflictions/AfflictionPrefab.cs index ad1261170..aea153f53 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -151,7 +151,7 @@ namespace Barotrauma //how high the strength has to be for the affliction to take affect public readonly float ActivationThreshold = 0.0f; //how high the strength has to be for the affliction icon to be shown in the UI - public readonly float ShowIconThreshold = 0.0f; + public readonly float ShowIconThreshold = 0.05f; public readonly float MaxStrength = 100.0f; public float BurnOverlayAlpha; @@ -254,7 +254,7 @@ namespace Barotrauma } ActivationThreshold = element.GetAttributeFloat("activationthreshold", 0.0f); - ShowIconThreshold = element.GetAttributeFloat("showiconthreshold", ActivationThreshold); + ShowIconThreshold = element.GetAttributeFloat("showiconthreshold", Math.Max(ActivationThreshold, 0.05f)); MaxStrength = element.GetAttributeFloat("maxstrength", 100.0f); DamageOverlayAlpha = element.GetAttributeFloat("damageoverlayalpha", 0.0f); diff --git a/Barotrauma/BarotraumaShared/Source/Utils/MathUtils.cs b/Barotrauma/BarotraumaShared/Source/Utils/MathUtils.cs index 31760c6d5..bb12617b7 100644 --- a/Barotrauma/BarotraumaShared/Source/Utils/MathUtils.cs +++ b/Barotrauma/BarotraumaShared/Source/Utils/MathUtils.cs @@ -753,6 +753,15 @@ namespace Barotrauma } } + /// + /// Float comparison. Note that may still fail in some cases. + /// + public static bool NearlyEqual(Vector2 a, Vector2 b, float epsilon = 0.0001f) + { + return NearlyEqual(a.X, b.X, epsilon) && NearlyEqual(a.Y, b.Y, epsilon); + } + + /// /// Returns a position in a curve. /// public static Vector2 Bezier(Vector2 start, Vector2 control, Vector2 end, float t)