From cb6dcd469ebef046ca809a487470c998e02b2b0d Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Tue, 19 Mar 2019 16:50:15 +0200 Subject: [PATCH] af8a4af...8a2a718 commit 8a2a718a0544d5c1707f10b75498e528a7912726 Author: Joonas Rikkonen Date: Tue Mar 19 16:47:49 2019 +0200 Only update affliction icons every 0.5 seconds (or when switching limb selection in the health interface). Otherwise afflictions can flicker rapidly when the player for example has status effects that cause an affliction's strength to fluctuate around the ShowIconThreshold. Also set the default ShowIconThreshold value to 0.05. Closes #1304 --- .../BarotraumaClient/ClientCode.projitems | 3 +- .../Characters/Health/CharacterHealth.cs | 1410 ++++++++++++++++- .../Health/Afflictions/AfflictionPrefab.cs | 4 +- .../Source/Utils/MathUtils.cs | 9 + 4 files changed, 1421 insertions(+), 5 deletions(-) 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)