using Barotrauma.Extensions; using Barotrauma.Items.Components; using Barotrauma.Networking; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using System.Linq; namespace Barotrauma { partial class CharacterHealth { private static bool toggledThisFrame; public class DamageOverlayPrefab : Prefab { public readonly static PrefabSelector Prefabs = new PrefabSelector(); public readonly Sprite DamageOverlay; public DamageOverlayPrefab(ContentXElement element, AfflictionsFile file) : base(file, file.Path.Value.ToIdentifier()) { DamageOverlay = new Sprite(element); } public override void Dispose() { DamageOverlay.Remove(); } } public static Sprite DamageOverlay => DamageOverlayPrefab.Prefabs.ActivePrefab.DamageOverlay; private Point screenResolution; private float uiScale, inventoryScale; private Alignment alignment = Alignment.Right; public Alignment Alignment { get { return alignment; } set { if (alignment == value) { return; } alignment = value; UpdateAlignment(); } } public GUIButton SuicideButton { get; private set; } // healthbars private GUIProgressBar healthBar; private GUIProgressBar healthBarShadow; private float healthShadowSize; private float healthShadowDelay; private float healthBarPulsateTimer; private float healthBarPulsatePhase; private float bloodParticleTimer; private GUIFrame healthWindow; private GUITextBlock deadIndicator; //private GUIComponent lowSkillIndicator; private GUIButton cprButton; private GUIListBox afflictionTooltip; private static readonly Color oxygenLowGrainColor = new Color(0.1f, 0.1f, 0.1f, 1f); private SpriteSheet limbIndicatorOverlay; private float limbIndicatorOverlayAnimState; private SpriteSheet medUIExtra; private float medUIExtraAnimState; private int highlightedLimbIndex = -1; private int selectedLimbIndex = -1; private LimbHealth currentDisplayedLimb; /// /// Container for the icons above the health bar /// private GUIComponent afflictionIconContainer; private float afflictionIconRefreshTimer; const float AfflictionIconRefreshInterval = 1.0f; private GUIButton showHiddenAfflictionsButton; /// /// Container for passive afflictions that have been hidden from afflictionIconContainer /// private GUIComponent hiddenAfflictionIconContainer; private GUIProgressBar healthWindowHealthBar; private GUIProgressBar healthWindowHealthBarShadow; private GUITextBlock characterName; private GUIListBox afflictionIconList; private GUILayoutGroup treatmentLayout; private GUIListBox recommendedTreatmentContainer; /// /// Timer for updating visuals (limb tints and overlays) caused by the affliction /// private float updateVisualsTimer = Rand.Range(0.0f, UpdateVisualsInterval); const float UpdateVisualsInterval = 0.5f; private float distortTimer; // 0-1 private float damageIntensity; private readonly float damageIntensityDropdownRate = 0.1f; public float DamageOverlayTimer { get; private set; } private float updateDisplayedAfflictionsTimer; private const float UpdateDisplayedAfflictionsInterval = 0.5f; private List currentDisplayedAfflictions = new List(); public float DisplayedVitality, DisplayVitalityDelay; public bool MouseOnElement { get { return highlightedLimbIndex > -1; } } private static CharacterHealth openHealthWindow; public static CharacterHealth OpenHealthWindow { get { return openHealthWindow; } set { if (openHealthWindow == value) { return; } if (value != null) { if (!value.UseHealthWindow || value.Character.DisableHealthWindow) { return; } } var prevOpenHealthWindow = openHealthWindow; if (prevOpenHealthWindow != null) { prevOpenHealthWindow.highlightedLimbIndex = -1; } openHealthWindow = value; toggledThisFrame = true; if (Character.Controlled == null) { return; } if (value == null && Character.Controlled?.SelectedCharacter?.CharacterHealth != null && Character.Controlled.SelectedCharacter.CharacterHealth == prevOpenHealthWindow) { Character.Controlled.DeselectCharacter(); } Character.Controlled.DisableInteract = true; if (openHealthWindow != null) { if (value.Character.Info == null || value.Character == Character.Controlled || Character.Controlled.HasEquippedItem("healthscanner".ToIdentifier())) { openHealthWindow.characterName.Text = value.Character.Name; } else { openHealthWindow.characterName.Text = value.Character.Info.DisplayName; value.Character.Info.CheckDisguiseStatus(false); } Character.Controlled.SelectedItem = null; } HintManager.OnShowHealthInterface(); } } public GUIButton CPRButton { get { return cprButton; } } public GUIComponent InventorySlotContainer { get; private set; } public float HealthBarPulsateTimer { get { return healthBarPulsateTimer; } set { healthBarPulsateTimer = MathHelper.Clamp(value, 0.0f, 10.0f); } } private GUIFrame healthBarHolder; partial void InitProjSpecific(ContentXElement element, Character character) { DisplayedVitality = MaxVitality; character.OnAttacked += OnAttacked; healthWindow = new GUIFrame(new RectTransform(new Vector2(0.35f, 0.6f), GUI.Canvas, anchor: Anchor.Center, scaleBasis: ScaleBasis.Smallest), style: "GUIFrameListBox"); var healthWindowVerticalLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.95f), healthWindow.RectTransform, Anchor.Center)) { Stretch = true }; var nameContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.2f), healthWindowVerticalLayout.RectTransform) { MinSize = new Point(0, 20) }, isHorizontal: true) { Stretch = true }; new GUICustomComponent(new RectTransform(new Vector2(0.2f, 1.0f), nameContainer.RectTransform, Anchor.CenterLeft), onDraw: (spriteBatch, component) => { character.Info?.DrawIcon(spriteBatch, component.Rect.Center.ToVector2(), component.Rect.Size.ToVector2()); }); characterName = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), nameContainer.RectTransform), "", textAlignment: Alignment.CenterLeft, font: GUIStyle.SubHeadingFont) { AutoScaleHorizontal = true }; new GUICustomComponent(new RectTransform(new Vector2(0.2f, 1.0f), nameContainer.RectTransform), onDraw: (spriteBatch, component) => { character.Info?.DrawJobIcon(spriteBatch, component.Rect, character != Character.Controlled); }); var healthBarContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.07f), healthWindowVerticalLayout.RectTransform), style: null); var healthBarIcon = new GUIFrame(new RectTransform(new Vector2(0.095f, 1.0f), healthBarContainer.RectTransform), style: "GUIHealthBarIcon"); healthWindowHealthBarShadow = new GUIProgressBar(new RectTransform(new Vector2(0.91f, 1.0f), healthBarContainer.RectTransform, Anchor.CenterRight), barSize: 1.0f, color: GUIStyle.Green, style: "GUIHealthBar") { IsHorizontal = true }; healthWindowHealthBar = new GUIProgressBar(new RectTransform(new Vector2(0.91f, 1.0f), healthBarContainer.RectTransform, Anchor.CenterRight), barSize: 1.0f, color: GUIStyle.Green, style: "GUIHealthBar") { IsHorizontal = true }; //spacing new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), healthWindowVerticalLayout.RectTransform), style: null); var characterIndicatorArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.95f), healthWindowVerticalLayout.RectTransform), isHorizontal: true) { Stretch = true, //RelativeSpacing = 0.05f }; InventorySlotContainer = new GUICustomComponent(new RectTransform(new Vector2(0.1f, 1.0f), characterIndicatorArea.RectTransform, Anchor.TopLeft, Pivot.TopRight), (spriteBatch, component) => { for (int i = 0; i < character.Inventory.Capacity; i++) { if (character.Inventory.SlotTypes[i] != InvSlotType.HealthInterface) { continue; } if (character.Inventory.HideSlot(i)) { continue; } //don't draw the item if it's being dragged out of the slot bool drawItem = !Inventory.DraggingItems.Any() || !Character.Inventory.GetItemsAt(i).All(it => Inventory.DraggingItems.Contains(it)) || character.Inventory.visualSlots[i].MouseOn(); Inventory.DrawSlot(spriteBatch, Character.Inventory, Character.Inventory.visualSlots[i], Character.Inventory.GetItemAt(i), i, drawItem, Character.Inventory.SlotTypes[i]); if (medUIExtra != null) { float overlayScale = Math.Min( Character.Inventory.visualSlots[i].Rect.Width / (float)medUIExtra.FrameSize.X, Character.Inventory.visualSlots[i].Rect.Height / (float)medUIExtra.FrameSize.Y); int frame = (int)medUIExtraAnimState; medUIExtra.Draw(spriteBatch, frame, Character.Inventory.visualSlots[i].Rect.Center.ToVector2(), Color.Gray, origin: medUIExtra.FrameSize.ToVector2() / 2, rotate: 0.0f, scale: Vector2.One * overlayScale); } } }, (dt, component) => { if (!GameMain.Instance.Paused) { medUIExtraAnimState = (medUIExtraAnimState + dt * 10.0f) % 16.0f; } }); cprButton = new GUIButton(new RectTransform(new Vector2(0.17f, 0.17f), characterIndicatorArea.RectTransform, Anchor.BottomLeft, scaleBasis: ScaleBasis.Smallest), text: "", style: "CPRButton") { UserData = UIHighlightAction.ElementId.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 Character.TreatmentEventData()); } return true; }, ToolTip = TextManager.Get("doctor.cprobjective"), IgnoreLayoutGroups = true, Visible = false }; var limbSelection = new GUICustomComponent(new RectTransform(new Vector2(0.4f, 1.0f), characterIndicatorArea.RectTransform), (spriteBatch, component) => { DrawHealthWindow(spriteBatch, component.RectTransform.Rect, true); }, (deltaTime, component) => { UpdateLimbIndicators(deltaTime, component.RectTransform.Rect); } ); deadIndicator = new GUITextBlock(new RectTransform(new Vector2(0.9f, 0.1f), limbSelection.RectTransform, Anchor.Center), text: TextManager.Get("Deceased"), font: GUIStyle.LargeFont, textAlignment: Alignment.Center, style: "GUIToolTip") { Visible = false, CanBeFocused = false }; if (deadIndicator.Text.Contains(' ')) { deadIndicator.Wrap = true; } else { deadIndicator.AutoScaleHorizontal = true; } afflictionIconList = new GUIListBox(new RectTransform(new Vector2(0.25f, 1.0f), characterIndicatorArea.RectTransform), style: null); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), healthWindowVerticalLayout.RectTransform), TextManager.Get("SuitableTreatments"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomCenter); treatmentLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), healthWindowVerticalLayout.RectTransform), true) { Stretch = false }; recommendedTreatmentContainer = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), treatmentLayout.RectTransform, Anchor.Center, Pivot.Center), isHorizontal: true, style: null) { Spacing = GUI.IntScale(4), KeepSpaceForScrollBar = false, ScrollBarVisible = false, AutoHideScrollBar = false }; new GUITextBlock(new RectTransform(Vector2.One, recommendedTreatmentContainer.Content.RectTransform), TextManager.Get("none"), textAlignment: Alignment.Center) { CanBeFocused = false }; characterIndicatorArea.Recalculate(); healthBarHolder = new GUIFrame(new RectTransform(Point.Zero, GUI.Canvas), style: null) { HoverCursor = CursorState.Hand }; healthBarHolder.RectTransform.AbsoluteOffset = HUDLayoutSettings.HealthBarArea.Location; healthBarHolder.RectTransform.NonScaledSize = HUDLayoutSettings.HealthBarArea.Size; healthBarHolder.RectTransform.RelativeOffset = Vector2.Zero; healthBarShadow = new GUIProgressBar(new RectTransform(Vector2.One, healthBarHolder.RectTransform, Anchor.BottomRight), barSize: 1.0f, color: Color.Green, style: "CharacterHealthBar", showFrame: false) { Visible = false }; healthShadowSize = 1.0f; healthBar = new GUIProgressBar(new RectTransform(Vector2.One, healthBarHolder.RectTransform, Anchor.BottomRight), barSize: 1.0f, color: GUIStyle.HealthBarColorHigh, style: "CharacterHealthBar") { HoverCursor = CursorState.Hand, ToolTip = TextManager.GetWithVariable("hudbutton.healthinterface", "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Health)), Enabled = true }; afflictionIconContainer = new GUILayoutGroup( HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.HealthBarAfflictionArea, GUI.Canvas), isHorizontal: true, childAnchor: Anchor.CenterRight) { AbsoluteSpacing = GUI.IntScale(5) }; showHiddenAfflictionsButton = new GUIButton(new RectTransform(new Point(afflictionIconContainer.Rect.Height), afflictionIconContainer.RectTransform), style: "GUIButtonCircular") { Visible = false, CanBeFocused = false }; hiddenAfflictionIconContainer = new GUILayoutGroup( HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.HealthBarAfflictionArea, GUI.Canvas), isHorizontal: true, childAnchor: Anchor.CenterRight) { AbsoluteSpacing = GUI.IntScale(5) }; UpdateAlignment(); SuicideButton = new GUIButton(new RectTransform(new Vector2(0.1f, 0.02f), GUI.Canvas, Anchor.TopCenter) { MinSize = new Point(150, 20), RelativeOffset = new Vector2(0.0f, 0.01f) }, TextManager.Get("GiveInButton"), style: "GUIButtonLarge") { Visible = false, 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 Character.CharacterStatusEventData()); } else { var (type, affliction) = GetCauseOfDeath(); Character.Controlled.Kill(type, affliction); Character.Controlled = null; } } return true; } }; SuicideButton.TextBlock.AutoScaleHorizontal = true; if (element != null) { foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "sprite": case "meduisilhouette": limbIndicatorOverlay = new SpriteSheet(subElement); break; case "meduiextra": medUIExtra = new SpriteSheet(subElement); break; } } } healthWindowVerticalLayout.Recalculate(); } private void OnAttacked(Character attacker, AttackResult attackResult) { if (Math.Abs(attackResult.Damage) < 0.01f) { return; } if (ShowDamageOverlay) { DamageOverlayTimer = MathHelper.Clamp(attackResult.Damage / MaxVitality, DamageOverlayTimer, 1.0f); float additionalIntensity = MathHelper.Lerp(0, 1, MathUtils.InverseLerp(0, 0.1f, attackResult.Damage / MaxVitality)); damageIntensity = MathHelper.Clamp(damageIntensity + additionalIntensity, 0, 1); } if (healthShadowDelay <= 0.0f) { healthShadowDelay = 1.0f; } if (healthBarPulsateTimer <= 0.0f) { healthBarPulsatePhase = 0.0f; } healthBarPulsateTimer = 1.0f; DisplayVitalityDelay = 0.5f; } private void UpdateAlignment() { screenResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); inventoryScale = Inventory.UIScale; uiScale = GUI.Scale; showHiddenAfflictionsButton.RectTransform.NonScaledSize = new Point(afflictionIconContainer.Rect.Height); //remove affliction icons so we recreate and resize them for (int i = afflictionIconContainer.CountChildren - 1; i >= 0; i--) { var child = afflictionIconContainer.GetChild(i); if (child.UserData is AfflictionPrefab) { afflictionIconContainer.RemoveChild(child); } } healthBarHolder.RectTransform.AbsoluteOffset = HUDLayoutSettings.HealthBarArea.Location; healthBarHolder.RectTransform.NonScaledSize = HUDLayoutSettings.HealthBarArea.Size; healthBarHolder.RectTransform.RelativeOffset = Vector2.Zero; switch (alignment) { case Alignment.Left: healthWindow.RectTransform.SetPosition(Anchor.BottomLeft); healthWindow.RectTransform.AbsoluteOffset = new Point(HUDLayoutSettings.InventoryAreaLower.X, screenResolution.Y - HUDLayoutSettings.ChatBoxArea.Y + HUDLayoutSettings.Padding); break; case Alignment.Right: healthWindow.RectTransform.SetPosition(Anchor.BottomRight); healthWindow.RectTransform.AbsoluteOffset = new Point(HUDLayoutSettings.Padding, screenResolution.Y - HUDLayoutSettings.ChatBoxArea.Y + HUDLayoutSettings.Padding); break; } healthWindow.RectTransform.RecalculateChildren(false); Character.Inventory?.RefreshSlotPositions(); } public void UpdateClientSpecific(float deltaTime) { if (GameMain.NetworkMember == null) { DisplayedVitality = Vitality; } else { DisplayVitalityDelay -= deltaTime; if (DisplayVitalityDelay <= 0.0f) { DisplayedVitality = Vitality; } } if (damageIntensity > 0) { damageIntensity -= deltaTime * damageIntensityDropdownRate; if (damageIntensity < 0) { damageIntensity = 0; } } if (DamageOverlayTimer > 0.0f) { DamageOverlayTimer -= deltaTime; } } private float timeUntilNextHeartbeatSound = 0.0f; private bool nextHeartbeatSoundIsSystole = true; private const string diastoleSoundTag = "heartbeatdiastole", systoleSoundTag = "heartbeatsystole"; partial void UpdateOxygenProjSpecific(float prevOxygen, float deltaTime) { if (prevOxygen > 0.0f && OxygenAmount <= 0.0f && Character.Controlled == Character) { string soundName; if (Character.Info != null) { soundName = Character.Info.ReplaceVars($"drown[{Character.Info.Prefab.MenuCategoryVar}]"); } else { var charInfoPrefab = CharacterPrefab.HumanPrefab.CharacterInfoPrefab; soundName = charInfoPrefab.ReplaceVars($"drown[{charInfoPrefab.MenuCategoryVar}]", charInfoPrefab.Heads.First()); } SoundPlayer.PlaySound(soundName); } if (Character == Character.Controlled && !IsUnconscious && !Character.IsDead && OxygenAmount < LowOxygenThreshold) { timeUntilNextHeartbeatSound -= deltaTime; if (timeUntilNextHeartbeatSound < 0.0f) { if (nextHeartbeatSoundIsSystole) { SoundPlayer.PlaySound(systoleSoundTag, 1.0f - (OxygenAmount / LowOxygenThreshold)); timeUntilNextHeartbeatSound = MathHelper.Lerp(0.18f, 0.3f, Math.Clamp(OxygenAmount / InsufficientOxygenThreshold, 0.0f, 1.0f)); } else { SoundPlayer.PlaySound(diastoleSoundTag, 1.0f - (OxygenAmount / LowOxygenThreshold)); timeUntilNextHeartbeatSound = MathHelper.Lerp(0.3f, 0.5f, Math.Clamp(OxygenAmount / InsufficientOxygenThreshold, 0.0f, 1.0f)); } nextHeartbeatSoundIsSystole = !nextHeartbeatSoundIsSystole; } } } partial void UpdateBleedingProjSpecific(AfflictionBleeding affliction, Limb targetLimb, float deltaTime) { if (Character.InvisibleTimer > 0.0f) { return; } bloodParticleTimer -= deltaTime * (affliction.Strength / 10.0f); if (bloodParticleTimer <= 0.0f) { Limb limb = targetLimb ?? Character.AnimController.MainLimb; bool inWater = Character.AnimController.InWater; var drawTarget = inWater ? Particles.ParticlePrefab.DrawTargetType.Water : Particles.ParticlePrefab.DrawTargetType.Air; var emitter = Character.BloodEmitters.FirstOrDefault(e => e.Prefab.ParticlePrefab?.DrawTarget == drawTarget || e.Prefab.ParticlePrefab?.DrawTarget == Particles.ParticlePrefab.DrawTargetType.Both); float particleMinScale = emitter?.Prefab.Properties.ScaleMin ?? 0.5f; float particleMaxScale = emitter?.Prefab.Properties.ScaleMax ?? 1; float severity = Math.Min(affliction.Strength / affliction.Prefab.MaxStrength * Character.Params.BleedParticleMultiplier, 1); float bloodParticleSize = MathHelper.Lerp(particleMinScale, particleMaxScale, severity); Vector2 velocity = Rand.Vector(affliction.Strength * 0.1f); if (!inWater) { bloodParticleSize *= 2.0f; velocity = limb.LinearVelocity * 100.0f; } // TODO: use the blood emitter? var blood = GameMain.ParticleManager.CreateParticle( inWater ? Character.Params.BleedParticleWater : Character.Params.BleedParticleAir, limb.WorldPosition, velocity, 0.0f, Character.AnimController.CurrentHull); if (blood != null) { blood.Size *= bloodParticleSize; if (!inWater && !string.IsNullOrEmpty(Character.BloodDecalName) && Rand.Range(0.0f, 1.0f) < 0.05f) { blood.OnCollision += (Vector2 pos, Hull hull) => { var decal = hull?.AddDecal(Character.BloodDecalName, pos, Rand.Range(1.0f, 2.0f), isNetworkEvent: true); if (decal != null) { decal.FadeTimer = decal.LifeTime - decal.FadeOutTime * 2; } }; } } bloodParticleTimer = MathHelper.Lerp(2, 0.5f, severity); } } public static bool IsMouseOnHealthBar() { if (Character.Controlled?.CharacterHealth == null) { return false; } return Character.Controlled.CharacterHealth.healthBar.State == GUIComponent.ComponentState.Hover; } 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(mergeSameAfflictions: true, predicate: a => a.ShouldShowIcon(Character) && a.Prefab.Icon != null); currentDisplayedAfflictions.Sort((a1, a2) => { int dmgPerSecond = Math.Sign(a1.DamagePerSecond - a2.DamagePerSecond); if (dmgPerSecond != 0) { return dmgPerSecond; } return Math.Sign(GetStr(a1) - GetStr(a2)); static float GetStr(Affliction affliction) { return affliction.Strength / affliction.Prefab.MaxStrength * (affliction.Prefab.IsBuff ? 1.0f : 10.0f); } }); HintManager.OnAfflictionDisplayed(Character, currentDisplayedAfflictions); updateDisplayedAfflictionsTimer = UpdateDisplayedAfflictionsInterval; } if (healthShadowDelay > 0.0f) { healthShadowDelay -= deltaTime; } else { healthShadowSize = healthBar.BarSize > healthShadowSize ? Math.Min(healthShadowSize + deltaTime, healthBar.BarSize) : Math.Max(healthShadowSize - deltaTime, healthBar.BarSize); } float blurStrength = 0.0f; float distortStrength = 0.0f; float distortSpeed = 0.0f; float radialDistortStrength = 0.0f; float chromaticAberrationStrength = 0.0f; float grainStrength = 0.0f; Color grainColor = Color.Transparent; float oxygenLowStrength = 0.0f; if (Character.IsUnconscious) { blurStrength = 1.0f; distortSpeed = 1.0f; } else if (OxygenAmount < 100.0f) { oxygenLowStrength = Math.Min(1.0f - (OxygenAmount - LowOxygenThreshold) / LowOxygenThreshold, 1.0f); blurStrength = MathHelper.Lerp(0.5f, 1.0f, 1.0f - Vitality / MaxVitality) * oxygenLowStrength; distortStrength = blurStrength * oxygenLowStrength; distortSpeed = blurStrength + 1.0f; distortSpeed *= distortSpeed * distortSpeed * distortSpeed; grainStrength = MathHelper.Lerp(0.5f, 10.0f, oxygenLowStrength); grainColor = oxygenLowGrainColor; } foreach (KeyValuePair kvp in afflictions) { var affliction = kvp.Key; distortStrength = Math.Max(distortStrength, affliction.GetScreenDistortStrength()); blurStrength = Math.Max(blurStrength, affliction.GetScreenBlurStrength()); radialDistortStrength = Math.Max(radialDistortStrength, affliction.GetRadialDistortStrength()); chromaticAberrationStrength = Math.Max(chromaticAberrationStrength, affliction.GetChromaticAberrationStrength()); float afflictionGrainStrength = affliction.GetScreenGrainStrength(); if (afflictionGrainStrength > 0.0f) { grainStrength = Math.Max(grainStrength, afflictionGrainStrength); Color afflictionGrainColor = affliction.GetActiveEffect()?.GrainColor ?? Color.White; grainColor = Color.Lerp(grainColor, afflictionGrainColor, (float)Math.Pow(1.0f - oxygenLowStrength, 2)); } } Character.RadialDistortStrength = radialDistortStrength; Character.ChromaticAberrationStrength = chromaticAberrationStrength; Character.GrainStrength = grainStrength; Character.GrainColor = grainColor; 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.05f * distortStrength; } else { Character.BlurStrength = 0.0f; Character.DistortStrength = 0.0f; distortTimer = 0.0f; } UpdateStatusHUD(deltaTime); if (PlayerInput.KeyHit(InputType.Health) && GUI.KeyboardDispatcher.Subscriber == null && Character.Controlled.AllowInput && !toggledThisFrame) { if (openHealthWindow != null) { OpenHealthWindow = null; } else if (Character.Controlled == Character && (Character.Controlled.FocusedCharacter?.CharacterHealth == null || !Character.Controlled.FocusedCharacter.CharacterHealth.UseHealthWindow || Character.Controlled.FocusedCharacter.DisableHealthWindow)) { OpenHealthWindow = this; forceAfflictionContainerUpdate = true; } } else if (openHealthWindow == this) { if (HUD.CloseHUD(healthWindow.Rect)) { //emulate a Health input to get the character to deselect the item server-side if (GameMain.Client != null) { Character.Controlled.EmulateInput(InputType.Health); } OpenHealthWindow = null; } foreach (GUIComponent afflictionIcon in afflictionIconList.Content.Children) { if (afflictionIcon.UserData is not Affliction affliction) { continue; } if (affliction.AppliedAsFailedTreatmentTime > Timing.TotalTime - 1.0 && afflictionIcon.FlashTimer <= 0.0f) { afflictionIcon.Flash(GUIStyle.Red); } else if (affliction.AppliedAsSuccessfulTreatmentTime > Timing.TotalTime - 1.0 && afflictionIcon.FlashTimer <= 0.0f) { afflictionIcon.Flash(GUIStyle.Green); } } if (GUI.MouseOn?.UserData is Affliction) { Affliction affliction = GUI.MouseOn?.UserData as Affliction; if (afflictionTooltip == null || afflictionTooltip.UserData != affliction) { afflictionTooltip = new GUIListBox(new RectTransform(new Vector2(0.4f, 0.2f), GUI.Canvas, scaleBasis: ScaleBasis.Smallest)) { UserData = affliction, CanBeFocused = false }; CreateAfflictionInfoElements(afflictionTooltip.Content, affliction); int height = afflictionTooltip.Content.Children.Sum(c => c.Rect.Height) + 10; afflictionTooltip.RectTransform.Resize(new Point(afflictionTooltip.Rect.Width, height), true); if (Alignment == Alignment.Right) { afflictionTooltip.RectTransform.AbsoluteOffset = new Point(GUI.MouseOn.Rect.X, GUI.MouseOn.Rect.Y); afflictionTooltip.RectTransform.Pivot = Pivot.TopRight; } else { afflictionTooltip.RectTransform.AbsoluteOffset = new Point(GUI.MouseOn.Rect.Right, GUI.MouseOn.Rect.Y); afflictionTooltip.RectTransform.Anchor = Anchor.TopLeft; } afflictionTooltip.ScrollBarVisible = false; var labelContainer = afflictionTooltip.Content.GetChildByUserData("label"); labelContainer.RectTransform.Resize(new Point(labelContainer.Rect.Width, (int)(GUIStyle.LargeFont.Size * 1.5f))); } } else { afflictionTooltip = 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 = SortAfflictionsBySeverity(GetAllAfflictions(a => a.Prefab.IndicatorLimb != LimbType.None)).FirstOrDefault(); if (affliction != null && (affliction.DamagePerSecond > 0 || affliction.Strength > 0)) { var limbHealth = GetMatchingLimbHealth(affliction); if (limbHealth != null) { selectedLimbIndex = limbHealths.IndexOf(limbHealth); } } else { // If no affliction is critical, select the limb which has most damage. var limbHealth = limbHealths.OrderByDescending(l => GetTotalDamage(l)).FirstOrDefault(); selectedLimbIndex = limbHealths.IndexOf(limbHealth); } } LimbHealth selectedLimb = selectedLimbIndex < 0 ? highlightedLimb : limbHealths[selectedLimbIndex]; if (selectedLimb != currentDisplayedLimb || forceAfflictionContainerUpdate) { UpdateAfflictionContainer(selectedLimb); currentDisplayedLimb = selectedLimb; } UpdateAfflictionInfos(displayedAfflictions.Select(d => d.affliction)); foreach (GUIComponent component in recommendedTreatmentContainer.Content.Children) { var treatmentButton = component.GetChild(); if (treatmentButton?.UserData is not ItemPrefab itemPrefab) { continue; } var matchingItem = AIObjectiveRescue.FindMedicalItem(Character.Controlled.Inventory, itemPrefab.Identifier); treatmentButton.Enabled = matchingItem != null; if (treatmentButton.Enabled && treatmentButton.State == GUIComponent.ComponentState.Hover) { //highlight the slot the treatment item is in var rootContainer = matchingItem.RootContainer ?? matchingItem; var index = Character.Controlled.Inventory.FindIndex(rootContainer); if (Character.Controlled.Inventory.visualSlots != null && index > -1 && index < Character.Controlled.Inventory.visualSlots.Length && Character.Controlled.Inventory.visualSlots[index].HighlightTimer <= 0.0f) { Character.Controlled.Inventory.visualSlots[index].ShowBorderHighlight(GUIStyle.Green, 0.5f, 0.5f); } } if (matchingItem != null && !treatmentButton.ToolTip.IsNullOrEmpty()) { continue; } treatmentButton.ToolTip = RichString.Rich($"‖color:255,255,255,255‖{itemPrefab.Name}‖color:end‖" + '\n' + itemPrefab.Description); if (treatmentButton.Enabled) { treatmentButton.ToolTip = RichString.Rich( $"‖color:gui.green‖[{PlayerInput.PrimaryMouseLabel}] " + $"{TextManager.Get("quickuseaction.usetreatment")}‖color:end‖" + '\n' + treatmentButton.ToolTip.NestedStr); } foreach (GUIComponent child in treatmentButton.Children) { child.Enabled = treatmentButton.Enabled; } } } if (Character.IsDead) { healthBar.Color = healthWindowHealthBar.Color = Color.Black; healthBar.BarSize = healthWindowHealthBar.BarSize = 1.0f; } else { healthBar.Color = healthWindowHealthBar.Color = ToolBox.GradientLerp(DisplayedVitality / MaxVitality, GUIStyle.HealthBarColorLow, GUIStyle.HealthBarColorMedium, GUIStyle.HealthBarColorHigh); healthBar.HoverColor = healthWindowHealthBar.HoverColor = healthBar.Color * 2.0f; healthBar.BarSize = healthWindowHealthBar.BarSize = (DisplayedVitality > 0.0f) ? (MaxVitality > 0.0f ? DisplayedVitality / MaxVitality : 0.0f) : (Math.Abs(MinVitality) > 0.0f ? 1.0f - DisplayedVitality / 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; } if (Inventory.DraggingItems.Any()) { if (highlightedLimbIndex > -1) { selectedLimbIndex = highlightedLimbIndex; } } } else { if (openHealthWindow != null && Character != Character.Controlled && Character != Character.Controlled?.SelectedCharacter) { openHealthWindow = null; } highlightedLimbIndex = -1; } healthBarHolder.CanBeFocused = healthBar.CanBeFocused = healthBarShadow.CanBeFocused = !Character.ShouldLockHud(); if (Character.AllowInput && UseHealthWindow && !Character.DisableHealthWindow && healthBar.Enabled && healthBar.CanBeFocused && (GUI.IsMouseOn(healthBar) || GUI.MouseOn?.UserData is AfflictionPrefab) && Inventory.SelectedSlot == null) { healthBar.State = GUIComponent.ComponentState.Hover; if (PlayerInput.PrimaryMouseButtonClicked()) { OpenHealthWindow = openHealthWindow == this ? null : this; } } else { healthBar.State = GUIComponent.ComponentState.None; } SuicideButton.Visible = Character == Character.Controlled && !Character.IsDead && Character.IsIncapacitated; if (GameMain.GameSession?.Campaign is { } campaign) { RectTransform endRoundButton = campaign?.EndRoundButton?.RectTransform; RectTransform readyCheckButton = campaign?.ReadyCheckButton?.RectTransform; if (endRoundButton != null) { if (SuicideButton.Visible) { Point offset = new Point(0, SuicideButton.Rect.Height); endRoundButton.ScreenSpaceOffset = offset; } else if (endRoundButton.ScreenSpaceOffset != Point.Zero) { endRoundButton.ScreenSpaceOffset = Point.Zero; } if (readyCheckButton != null) { readyCheckButton.ScreenSpaceOffset = endRoundButton.ScreenSpaceOffset; } } } cprButton.Visible = Character == Character.Controlled?.SelectedCharacter && !Character.IsDead && Character.IsKnockedDown && openHealthWindow == this; cprButton.Selected = Character.Controlled != null && Character == Character.Controlled.SelectedCharacter && Character.Controlled.AnimController.Anim == AnimController.Animation.CPR; deadIndicator.Visible = Character.IsDead; } public void AddToGUIUpdateList() { if (GUI.DisableHUD) { return; } if (OpenHealthWindow == this) { healthWindow.AddToGUIUpdateList(); afflictionTooltip?.AddToGUIUpdateList(); } else if (Character.Controlled == Character && !CharacterHUD.IsCampaignInterfaceOpen) { healthBarHolder.AddToGUIUpdateList(); afflictionIconContainer.AddToGUIUpdateList(); if (hiddenAfflictionIconContainer.Visible) { hiddenAfflictionIconContainer.AddToGUIUpdateList(); } } if (SuicideButton.Visible && Character == Character.Controlled) { SuicideButton.AddToGUIUpdateList(); } if (cprButton != null && cprButton.Visible) { cprButton.AddToGUIUpdateList(); } } public void DrawHUD(SpriteBatch spriteBatch) { if (GUI.DisableHUD || Character.Removed) { 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(); } foreach (KeyValuePair kvp in afflictions) { var affliction = kvp.Key; if (affliction.Prefab is AfflictionPrefab { AfflictionOverlay: not null } afflictionPrefab) { Vector2 screenSize = new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight); if (afflictionPrefab.AfflictionOverlay is SpriteSheet spriteSheet) { spriteSheet.Draw(spriteBatch, spriteIndex: spriteSheet.GetAnimatedSpriteIndex(afflictionPrefab.AfflictionOverlayAnimSpeed), pos: Vector2.Zero, color: Color.White * affliction.GetAfflictionOverlayMultiplier(), origin: Vector2.Zero, rotate: 0, scale: screenSize / spriteSheet.FrameSize.ToVector2()); } else if (afflictionPrefab.AfflictionOverlay is Sprite sprite) { sprite.Draw(spriteBatch, pos: Vector2.Zero, color: Color.White * affliction.GetAfflictionOverlayMultiplier(), origin: Vector2.Zero, rotate: 0, scale: screenSize / sprite.size); } } var activeEffect = affliction.GetActiveEffect(); if (activeEffect is { ThermalOverlayRange: > 0.0f }) { StatusHUD.DrawThermalOverlay(spriteBatch, Character, Character, activeEffect.ThermalOverlayColor, activeEffect.ThermalOverlayRange, effectState: (float)Timing.TotalTimeUnpaused, showDeadCharacters: false); } } float damageOverlayAlpha = DamageOverlayTimer; if (Vitality < MaxVitality * 0.1f) { damageOverlayAlpha = Math.Max(1.0f - (Vitality / UnmodifiedMaxVitality * 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) { healthBar.RectTransform.ScreenSpaceOffset = healthBarShadow.RectTransform.ScreenSpaceOffset = Point.Zero; } if (healthBarHolder != null) { // If manning a turret the portrait doesn't get rendered so we push the health bar to remove the empty gap healthBarHolder.RectTransform.ScreenSpaceOffset = Character.ShouldLockHud() ? new Point(0, HUDLayoutSettings.PortraitArea.Height) : Point.Zero; } } //private (Affliction Affliction, LocalizedString NameToolTip)? highlightedAfflictionIcon = null; private readonly List statusIcons = new List(); private readonly Dictionary statusIconVisibleTime = new Dictionary(); private const float HideStatusIconDelay = 5.0f; public void UpdateStatusHUD(float deltaTime) { if (Character.Controlled?.SelectedCharacter == null && openHealthWindow == null) { statusIcons.Clear(); if (Character.InPressure) { statusIcons.Add(pressureAffliction); } if (Character.CurrentHull != null && Character.OxygenAvailable < LowOxygenThreshold && oxygenLowAffliction.Strength < oxygenLowAffliction.Prefab.ShowIconThreshold) { statusIcons.Add(oxygenLowAffliction); } foreach (Affliction affliction in currentDisplayedAfflictions) { statusIcons.Add(affliction); } int spacing = GUI.IntScale(10); if (Character.ShouldLockHud()) { // Push the icons down since the portrait doesn't get rendered afflictionIconContainer.RectTransform.ScreenSpaceOffset = new Point(0, HUDLayoutSettings.PortraitArea.Height); hiddenAfflictionIconContainer.RectTransform.ScreenSpaceOffset = new Point(0, -hiddenAfflictionIconContainer.Rect.Height - spacing + HUDLayoutSettings.PortraitArea.Height); } else { afflictionIconContainer.RectTransform.ScreenSpaceOffset = new Point(0, 0); hiddenAfflictionIconContainer.RectTransform.ScreenSpaceOffset = new Point(0, -hiddenAfflictionIconContainer.Rect.Height - spacing); } //remove affliction icons for afflictions that no longer exist RemoveNonExistentIcons(afflictionIconContainer); RemoveNonExistentIcons(hiddenAfflictionIconContainer); void RemoveNonExistentIcons(GUIComponent container) { for (int i = container.CountChildren - 1; i >= 0; i--) { var child = container.GetChild(i); if (child.UserData is not AfflictionPrefab afflictionPrefab) { continue; } if (!statusIcons.Any(s => s.Prefab == afflictionPrefab)) { container.RemoveChild(child); statusIconVisibleTime.Remove(afflictionPrefab); } } } foreach (var statusIcon in statusIcons) { Affliction affliction = statusIcon; AfflictionPrefab afflictionPrefab = affliction.Prefab; if (!statusIconVisibleTime.ContainsKey(afflictionPrefab)) { statusIconVisibleTime.Add(afflictionPrefab, 0.0f); } statusIconVisibleTime[afflictionPrefab] += deltaTime; Color color = GetAfflictionIconColor(afflictionPrefab, affliction); var matchingIcon = afflictionIconContainer.GetChildByUserData(afflictionPrefab) ?? hiddenAfflictionIconContainer.GetChildByUserData(afflictionPrefab); if (matchingIcon == null) { matchingIcon = new GUIButton(new RectTransform(new Point(afflictionIconContainer.Rect.Height), afflictionIconContainer.RectTransform), style: null) { UserData = afflictionPrefab, ToolTip = $"‖color:{color.ToStringHex()}‖{affliction.Prefab.Name}‖color:end‖", CanBeSelected = false }; if (affliction.Prefab.ShowDescriptionInTooltip) { matchingIcon.ToolTip = matchingIcon.ToolTip + "\n" + affliction.Prefab.GetDescription(affliction.Strength, AfflictionPrefab.Description.TargetType.Self); } if (affliction == pressureAffliction) { matchingIcon.ToolTip = TextManager.Get("PressureHUDWarning"); } else if (affliction == pressureAffliction) { matchingIcon.ToolTip = TextManager.Get("OxygenHUDWarning"); } matchingIcon.ToolTip = RichString.Rich(matchingIcon.ToolTip); new GUIImage(new RectTransform(Vector2.One, matchingIcon.RectTransform, Anchor.BottomCenter), afflictionPrefab.Icon, scaleToFit: true) { CanBeFocused = false }; } if (afflictionPrefab.HideIconAfterDelay && statusIconVisibleTime[afflictionPrefab] > HideStatusIconDelay) { matchingIcon.RectTransform.Parent = hiddenAfflictionIconContainer.RectTransform; } var image = matchingIcon.GetChild(); image.Color = color; image.HoverColor = Color.Lerp(image.Color, Color.White, 0.5f); if (affliction.DamagePerSecond > 1.0f && matchingIcon.FlashTimer <= 0.0f) { matchingIcon.Flash(useCircularFlash: true, flashDuration: 1.5f, flashRectInflate: Vector2.One * 15.0f * GUI.Scale); image.Pulsate(Vector2.One, Vector2.One * 1.2f, 1.0f); } } afflictionIconRefreshTimer -= deltaTime; if (afflictionIconRefreshTimer <= 0.0f) { afflictionIconContainer.RectTransform.SortChildren((r1, r2) => { if (r1.GUIComponent.UserData is not AfflictionPrefab prefab1) { return -1; } if (r2.GUIComponent.UserData is not AfflictionPrefab prefab2) { return 1; } var index1 = statusIcons.IndexOf(s => s.Prefab == prefab1); var index2 = statusIcons.IndexOf(s => s.Prefab == prefab2); return index1.CompareTo(index2); }); (afflictionIconContainer as GUILayoutGroup).NeedsToRecalculate = true; afflictionIconRefreshTimer = AfflictionIconRefreshInterval; } Rectangle hiddenAfflictionHoverArea = showHiddenAfflictionsButton.Rect; foreach (GUIComponent child in hiddenAfflictionIconContainer.Children) { hiddenAfflictionHoverArea = Rectangle.Union(hiddenAfflictionHoverArea, child.Rect); } afflictionIconContainer.Visible = true; hiddenAfflictionIconContainer.Visible = showHiddenAfflictionsButton.Rect.Contains(PlayerInput.MousePosition) || (hiddenAfflictionIconContainer.Visible && hiddenAfflictionHoverArea.Contains(PlayerInput.MousePosition)); showHiddenAfflictionsButton.Visible = hiddenAfflictionIconContainer.CountChildren > 0; showHiddenAfflictionsButton.IgnoreLayoutGroups = !showHiddenAfflictionsButton.Visible; showHiddenAfflictionsButton.Text = $"+{hiddenAfflictionIconContainer.CountChildren}"; if (Vitality > 0.0f) { float currHealth = healthBar.BarSize; Color prevColor = healthBar.Color; healthBarShadow.BarSize = healthShadowSize; healthBarShadow.Color = Color.Lerp(GUIStyle.Red, Color.Black, 0.5f); healthBarShadow.Visible = true; healthBar.BarSize = currHealth; healthBar.Color = prevColor; } else { healthBarShadow.Visible = false; } } else { afflictionIconContainer.Visible = hiddenAfflictionIconContainer.Visible = false; if (Vitality > 0.0f) { float currHealth = healthWindowHealthBar.BarSize; Color prevColor = healthWindowHealthBar.Color; healthWindowHealthBarShadow.BarSize = healthShadowSize; healthWindowHealthBarShadow.Color = GUIStyle.Red; healthWindowHealthBarShadow.Visible = true; healthWindowHealthBar.BarSize = currHealth; healthWindowHealthBar.Color = prevColor; } else { healthWindowHealthBarShadow.Visible = false; } } } public static Color GetAfflictionIconColor(AfflictionPrefab prefab, Affliction affliction) { return GetAfflictionIconColor(prefab, affliction.Strength); } public static Color GetAfflictionIconColor(AfflictionPrefab prefab, float afflictionStrength) { //use sqrt to make the color change rapidly when strength is low //(low strength is where seeing the severity of the affliction makes more difference - at high strengths the character is already unconscious or dead) float colorT = MathF.Sqrt(afflictionStrength / prefab.MaxStrength); // No specific colors, use generic if (prefab.IconColors == null) { if (prefab.IsBuff) { return ToolBox.GradientLerp(colorT, GUIStyle.BuffColorLow, GUIStyle.BuffColorMedium, GUIStyle.BuffColorHigh); } return ToolBox.GradientLerp(colorT, GUIStyle.DebuffColorLow, GUIStyle.DebuffColorMedium, GUIStyle.DebuffColorHigh); } return ToolBox.GradientLerp(colorT, prefab.IconColors); } public static Color GetAfflictionIconColor(Affliction affliction) => GetAfflictionIconColor(affliction.Prefab, affliction); private readonly List<(Affliction affliction, float strength)> displayedAfflictions = new List<(Affliction affliction, float strength)>(); private void UpdateAfflictionContainer(LimbHealth selectedLimb) { if (selectedLimb == null) { afflictionIconList.Content.ClearChildren(); return; } if (afflictionsDirty() || selectedLimb != currentDisplayedLimb) { var currentAfflictions = afflictions.Where(a => ShouldDisplayAfflictionOnLimb(a, selectedLimb)).Select(a => a.Key); CreateAfflictionInfos(currentAfflictions); CreateRecommendedTreatments(); } //update recommended treatments if the strength of some displayed affliction has changed by > 1 else if (displayedAfflictions.Any(d => Math.Abs(d.strength - d.affliction.Strength) > 1.0f)) { CreateRecommendedTreatments(); } bool afflictionsDirty() { //not displaying one of the current afflictions -> dirty foreach (KeyValuePair kvp in afflictions) { if (!ShouldDisplayAfflictionOnLimb(kvp, selectedLimb)) { continue; } if (!displayedAfflictions.Any(d => d.affliction == kvp.Key)) { return true; } } //displaying an affliction we no longer have -> dirty foreach ((Affliction affliction, float strength) in displayedAfflictions) { if (afflictions.None(a => a.Key == affliction && a.Key.ShouldShowIcon(Character))) { return true; } } return false; } } private void CreateAfflictionInfos(IEnumerable afflictions) { afflictionIconList.ClearChildren(); displayedAfflictions.Clear(); Affliction mostSevereAffliction = SortAfflictionsBySeverity(afflictions, excludeBuffs: false).FirstOrDefault(); GUIButton buttonToSelect = null; foreach (Affliction affliction in afflictions) { displayedAfflictions.Add((affliction, affliction.Strength)); var frame = new GUIButton(new RectTransform(new Vector2(1.0f, 0.25f), afflictionIconList.Content.RectTransform), style: "ListBoxElement") { UserData = affliction, OnClicked = SelectAffliction }; new GUIFrame(new RectTransform(Vector2.One, frame.RectTransform), style: "GUIFrameListBox") { CanBeFocused = false }; var content = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.85f), frame.RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) { Stretch = true, CanBeFocused = false }; var progressbarBg = new GUIProgressBar(new RectTransform(new Vector2(1.0f, 0.18f), content.RectTransform), 0.0f, GUIStyle.Green, style: "GUIAfflictionBar") { UserData = "afflictionstrengthprediction", CanBeFocused = false }; new GUIProgressBar(new RectTransform(Vector2.One, progressbarBg.RectTransform), 0.0f, Color.Transparent, showFrame: false, style: "GUIAfflictionBar") { UserData = "afflictionstrength", CanBeFocused = false }; //spacing new GUIFrame(new RectTransform(new Vector2(1.0f, 0.15f), content.RectTransform), style: null) { CanBeFocused = false }; if (affliction == mostSevereAffliction) { buttonToSelect = frame; } var afflictionIcon = new GUIImage(new RectTransform(Vector2.One * 0.8f, content.RectTransform), affliction.Prefab.Icon, scaleToFit: true) { Color = GetAfflictionIconColor(affliction), CanBeFocused = false }; afflictionIcon.PressedColor = afflictionIcon.Color; afflictionIcon.HoverColor = Color.Lerp(afflictionIcon.Color, Color.White, 0.6f); afflictionIcon.SelectedColor = Color.Lerp(afflictionIcon.Color, Color.White, 0.5f); var nameText = new GUITextBlock(new RectTransform(new Vector2(1.1f, 0.0f), content.RectTransform), affliction.Prefab.Name, font: GUIStyle.SmallFont, textAlignment: Alignment.BottomCenter) { CanBeFocused = false }; nameText.Text = ToolBox.LimitString(nameText.Text, nameText.Font, nameText.Rect.Width); nameText.RectTransform.MinSize = new Point(0, (int)(nameText.TextSize.Y)); nameText.RectTransform.SizeChanged += () => { nameText.Text = ToolBox.LimitString(nameText.Text, nameText.Font, nameText.Rect.Width); }; content.Recalculate(); } buttonToSelect?.OnClicked(buttonToSelect, buttonToSelect.UserData); afflictionIconList.RecalculateChildren(); } private void CreateRecommendedTreatments() { ItemPrefab prevHighlightedItem = null; if (GUI.MouseOn?.UserData is ItemPrefab && recommendedTreatmentContainer.Content.IsParentOf(GUI.MouseOn)) { prevHighlightedItem = (ItemPrefab)GUI.MouseOn.UserData; } recommendedTreatmentContainer.Content.ClearChildren(); float characterSkillLevel = Character.Controlled == null ? 0.0f : Character.Controlled.GetSkillLevel(Tags.MedicalSkill); //key = item identifier //float = suitability Dictionary treatmentSuitability = new Dictionary(); GetSuitableTreatments(treatmentSuitability, user: Character.Controlled, ignoreHiddenAfflictions: true, limb: selectedLimbIndex == -1 ? null : Character.AnimController.Limbs.Find(l => l.HealthIndex == selectedLimbIndex), checkTreatmentSuggestionThreshold: true, checkTreatmentThreshold: false); foreach (Identifier treatment in treatmentSuitability.Keys.ToList()) { //prefer suggestions for items the player has if (Character.Controlled.Inventory.FindItemByIdentifier(treatment, recursive: true) != null) { treatmentSuitability[treatment] *= 10.0f; } } if (!treatmentSuitability.Any()) { new GUITextBlock(new RectTransform(Vector2.One, recommendedTreatmentContainer.Content.RectTransform), TextManager.Get("none"), textAlignment: Alignment.Center) { CanBeFocused = false }; recommendedTreatmentContainer.ScrollBarVisible = false; recommendedTreatmentContainer.AutoHideScrollBar = false; } else { recommendedTreatmentContainer.ScrollBarVisible = true; recommendedTreatmentContainer.AutoHideScrollBar = true; } List> treatmentSuitabilities = treatmentSuitability.OrderByDescending(t => t.Value).ToList(); int count = 0; foreach (KeyValuePair treatment in treatmentSuitabilities) { //don't list negative treatments if (treatment.Value < 0) { continue; } count++; if (count > 5) { break; } if (MapEntityPrefab.FindByIdentifier(treatment.Key) is not ItemPrefab item) { continue; } var itemSlot = new GUIFrame(new RectTransform(new Vector2(1.0f / 6.0f, 1.0f), recommendedTreatmentContainer.Content.RectTransform, Anchor.TopLeft), style: null) { UserData = item }; var innerFrame = new GUIButton(new RectTransform(Vector2.One, itemSlot.RectTransform, Anchor.Center, Pivot.Center, scaleBasis: ScaleBasis.Smallest), style: "SubtreeHeader") { UserData = item, DisabledColor = Color.White * 0.1f, PlaySoundOnSelect = false, OnClicked = (btn, userdata) => { if (userdata is not ItemPrefab itemPrefab) { return false; } var item = AIObjectiveRescue.FindMedicalItem(Character.Controlled.Inventory, it => it.Prefab == itemPrefab); if (item == null) { return false; } Limb targetLimb = Character.AnimController.Limbs.FirstOrDefault(l => l.HealthIndex == selectedLimbIndex); item.ApplyTreatment(Character.Controlled, Character, targetLimb); SoundPlayer.PlayUISound(GUISoundType.Select); return true; } }; new GUIImage(new RectTransform(Vector2.One, innerFrame.RectTransform, Anchor.Center), style: "TalentBackgroundGlow") { CanBeFocused = false, Color = GUIStyle.Green, HoverColor = Color.White, PressedColor = Color.DarkGray, SelectedColor = Color.Transparent, DisabledColor = Color.Transparent }; 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), innerFrame.RectTransform, Anchor.Center), itemSprite, scaleToFit: true) { CanBeFocused = false, Color = itemColor * 0.9f, HoverColor = itemColor, SelectedColor = itemColor, DisabledColor = itemColor * 0.8f }; if (item == prevHighlightedItem) { innerFrame.State = GUIComponent.ComponentState.Hover; innerFrame.Children.ForEach(c => c.State = GUIComponent.ComponentState.Hover); } } recommendedTreatmentContainer.RecalculateChildren(); afflictionIconList.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); }); if (count > 0) { var treatmentIconSize = recommendedTreatmentContainer.Content.Children.Sum(c => c.Rect.Width + recommendedTreatmentContainer.Spacing); if (treatmentIconSize < recommendedTreatmentContainer.Content.Rect.Width) { var spacing = new GUIFrame(new RectTransform(new Point((recommendedTreatmentContainer.Content.Rect.Width - treatmentIconSize) / 2, 0), recommendedTreatmentContainer.Content.RectTransform), style: null) { CanBeFocused = false }; spacing.RectTransform.SetAsFirstChild(); } } } private void CreateAfflictionInfoElements(GUIComponent parent, Affliction affliction) { var labelContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.2f), parent.RectTransform), isHorizontal: true) { Stretch = true, AbsoluteSpacing = 10, UserData = "label", CanBeFocused = false }; var afflictionName = new GUITextBlock(new RectTransform(new Vector2(0.65f, 1.0f), labelContainer.RectTransform), affliction.Prefab.Name, textAlignment: Alignment.CenterLeft, font: GUIStyle.LargeFont) { CanBeFocused = false, AutoScaleHorizontal = true }; var afflictionStrength = new GUITextBlock(new RectTransform(new Vector2(0.35f, 0.6f), labelContainer.RectTransform), "", textAlignment: Alignment.TopRight, font: GUIStyle.SubHeadingFont) { UserData = "strength", CanBeFocused = false }; var vitality = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), labelContainer.RectTransform, Anchor.BottomRight), "", textAlignment: Alignment.BottomRight) { Padding = afflictionStrength.Padding, IgnoreLayoutGroups = true, UserData = "vitality", CanBeFocused = false }; var description = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), parent.RectTransform), RichString.Rich(affliction.Prefab.GetDescription( affliction.Strength, Character == Character.Controlled ? AfflictionPrefab.Description.TargetType.Self : AfflictionPrefab.Description.TargetType.OtherCharacter)), textAlignment: Alignment.TopLeft, wrap: true) { CanBeFocused = false }; if (description.Font.MeasureString(description.WrappedText).Y > description.Rect.Height) { description.Font = GUIStyle.SmallFont; } Point nameDims = new Point(afflictionName.Rect.Width, (int)(GUIStyle.LargeFont.Size * 1.5f)); afflictionStrength.Text = affliction.GetStrengthText(); Vector2 strengthDims = GUIStyle.SubHeadingFont.MeasureString(afflictionStrength.Text); labelContainer.RectTransform.Resize(new Point(labelContainer.Rect.Width, nameDims.Y)); afflictionName.RectTransform.Resize(new Point((int)(labelContainer.Rect.Width - strengthDims.X * 0.99f), nameDims.Y)); afflictionStrength.RectTransform.Resize(new Point(labelContainer.Rect.Width - afflictionName.Rect.Width, nameDims.Y)); afflictionStrength.TextColor = Color.Lerp(GUIStyle.Orange, GUIStyle.Red, affliction.Strength / affliction.Prefab.MaxStrength); description.RectTransform.Resize(new Point(description.Rect.Width, (int)(description.TextSize.Y + 10))); int vitalityDecrease = (int)GetVitalityDecreaseWithVitalityMultipliers(affliction); if (vitalityDecrease == 0) { vitality.Visible = false; } else { vitality.Visible = true; vitality.Text = TextManager.Get("Vitality") + " -" + vitalityDecrease; vitality.TextColor = vitalityDecrease <= 0 ? GUIStyle.Green : Color.Lerp(GUIStyle.Orange, GUIStyle.Red, affliction.Strength / affliction.Prefab.MaxStrength); } vitality.AutoDraw = true; } private bool SelectAffliction(GUIButton button, object userData) { bool selected = button.Selected; foreach (var child in afflictionIconList.Content.Children) { if (child is GUIButton btn) { btn.Selected = btn == button && !selected; } } return false; } private void UpdateAfflictionInfos(IEnumerable afflictions) { var potentialTreatment = Inventory.DraggingItems.FirstOrDefault(); if (potentialTreatment == null && GUI.MouseOn?.UserData is ItemPrefab itemPrefab) { potentialTreatment = Character.Controlled.Inventory.FindItem(it => it.Prefab == itemPrefab, recursive: true); } potentialTreatment ??= Inventory.SelectedSlot?.Item; foreach (Affliction affliction in afflictions) { float afflictionVitalityDecrease = GetVitalityDecreaseWithVitalityMultipliers(affliction); Color afflictionEffectColor = Color.White; if (afflictionVitalityDecrease > 0.0f) { afflictionEffectColor = GUIStyle.Red; } else if (afflictionVitalityDecrease < 0.0f) { afflictionEffectColor = GUIStyle.Green; } var child = afflictionIconList.Content.FindChild(affliction); var afflictionStrengthPredictionBar = child.GetChild().GetChildByUserData("afflictionstrengthprediction") as GUIProgressBar; afflictionStrengthPredictionBar.BarSize = 0.0f; var afflictionStrengthBar = afflictionStrengthPredictionBar.GetChildByUserData("afflictionstrength") as GUIProgressBar; afflictionStrengthBar.BarSize = affliction.Strength / affliction.Prefab.MaxStrength; afflictionStrengthBar.Color = afflictionEffectColor; float afflictionStrengthPrediction = GetAfflictionStrengthPrediction(potentialTreatment, affliction); if (!MathUtils.NearlyEqual(afflictionStrengthPrediction, affliction.Strength)) { float t = (float)Math.Max(0.5f, (Math.Sin(Timing.TotalTime * 5) + 1.0f) / 2.0f); if (afflictionStrengthPrediction < affliction.Strength) { afflictionStrengthBar.Color = afflictionEffectColor; afflictionStrengthPredictionBar.Color = GUIStyle.Blue * t; afflictionStrengthPredictionBar.BarSize = afflictionStrengthBar.BarSize; afflictionStrengthBar.BarSize = afflictionStrengthPrediction / affliction.Prefab.MaxStrength; } else { afflictionStrengthPredictionBar.Color = Color.Red * t; afflictionStrengthPredictionBar.BarSize = afflictionStrengthPrediction / affliction.Prefab.MaxStrength; } } if (!affliction.Prefab.ShowBarInHealthMenu) { afflictionStrengthBar.BarSize = 1f; } if (afflictionTooltip != null && afflictionTooltip.UserData == affliction) { UpdateAfflictionInfo(afflictionTooltip.Content, affliction); } } } private float GetAfflictionStrengthPrediction(Item item, Affliction affliction) { float strength = affliction.Strength; if (item == null) { return strength; } foreach (ItemComponent ic in item.Components) { if (ic.statusEffectLists == null) { continue; } if (!ic.statusEffectLists.TryGetValue(ActionType.OnUse, out List statusEffects)) { continue; } foreach (StatusEffect effect in statusEffects) { foreach (var reduceAffliction in effect.ReduceAffliction) { if (reduceAffliction.AfflictionIdentifier != affliction.Identifier && reduceAffliction.AfflictionIdentifier != affliction.Prefab.AfflictionType) { continue; } strength -= reduceAffliction.ReduceAmount * (effect.Duration > 0 ? effect.Duration : 1.0f); } foreach (var addAffliction in effect.Afflictions) { if (addAffliction.Prefab != affliction.Prefab) { continue; } strength += addAffliction.Strength * (effect.Duration > 0 ? effect.Duration : 1.0f); } } } return strength; } private void UpdateAfflictionInfo(GUIComponent parent, Affliction affliction) { var labelContainer = parent.GetChildByUserData("label"); var strengthText = labelContainer.GetChildByUserData("strength") as GUITextBlock; strengthText.Text = affliction.GetStrengthText(); strengthText.TextColor = Color.Lerp(GUIStyle.Orange, GUIStyle.Red, affliction.Strength / affliction.Prefab.MaxStrength); var vitalityText = labelContainer.GetChildByUserData("vitality") as GUITextBlock; int vitalityDecrease = (int)GetVitalityDecreaseWithVitalityMultipliers(affliction); if (vitalityDecrease == 0) { vitalityText.Visible = false; } else { vitalityText.Visible = true; vitalityText.Text = TextManager.Get("Vitality") + " -" + vitalityDecrease; vitalityText.TextColor = vitalityDecrease <= 0 ? GUIStyle.Green : Color.Lerp(GUIStyle.Orange, GUIStyle.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)) { 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; } } Limb targetLimb = Character.AnimController.Limbs.FirstOrDefault(l => l.HealthIndex == selectedLimbIndex) ?? Character.AnimController.MainLimb; item.ApplyTreatment(Character.Controlled, Character, targetLimb); return true; } private void UpdateLimbIndicators(float deltaTime, Rectangle drawArea) { if (!GameMain.Instance.Paused) { 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.PrimaryMouseButtonClicked() && highlightedLimbIndex > -1) { selectedLimbIndex = highlightedLimbIndex; } } private static readonly List afflictionsDisplayedOnLimb = new List(); private void DrawHealthWindow(SpriteBatch spriteBatch, Rectangle drawArea, bool allowHighlight) { if (Character.Removed) { return; } spriteBatch.End(); spriteBatch.Begin(SpriteSortMode.Immediate, blendState: BlendState.NonPremultiplied, rasterizerState: GameMain.ScissorTestEnable, effect: GameMain.GameScreen.GradientEffect); int i = 0; foreach (LimbHealth limbHealth in limbHealths) { if (limbHealth.IndicatorSprite == null) { continue; } Rectangle limbEffectiveArea = new Rectangle(limbHealth.IndicatorSprite.SourceRect.X + limbHealth.HighlightArea.X, limbHealth.IndicatorSprite.SourceRect.Y + limbHealth.HighlightArea.Y, limbHealth.HighlightArea.Width, limbHealth.HighlightArea.Height); float totalDamage = GetTotalDamage(limbHealth); float damageLerp = totalDamage > 0.0f ? MathHelper.Lerp(0.2f, 1.0f, totalDamage / 100.0f) : 0.0f; float negativeEffect = 0.0f, positiveEffect = 0.0f; foreach (KeyValuePair kvp in afflictions) { if (kvp.Value != limbHealth) { continue; } var affliction = kvp.Key; if (!affliction.ShouldShowIcon(Character)) { continue; } if (!affliction.Prefab.IsBuff) { negativeEffect += affliction.Strength * GetVitalityMultiplier(affliction, limbHealth); } else { positiveEffect += affliction.Strength * 0.2f; } } float midPoint = (float)limbEffectiveArea.Center.Y / (float)limbHealth.IndicatorSprite.Texture.Height; float fadeDist = 0.6f * (float)limbEffectiveArea.Height / (float)limbHealth.IndicatorSprite.Texture.Height; if (negativeEffect > 0.0f && negativeEffect < 5.0f) { negativeEffect = 10.0f; } if (positiveEffect > 0.0f && positiveEffect < 5.0f) { positiveEffect = 10.0f; } Color positiveColor = Color.Lerp(Color.Orange, Color.Lime, Math.Min(positiveEffect / 25.0f, 1.0f)); Color negativeColor = Color.Lerp(Color.Orange, Color.Red, Math.Min(negativeEffect / 25.0f, 1.0f)); Color color1 = Color.Orange; Color color2 = Color.Orange; if (negativeEffect + positiveEffect > 0.0f) { if (negativeEffect >= positiveEffect) { color1 = Color.Lerp(positiveColor, negativeColor, (negativeEffect - positiveEffect) / negativeEffect); color2 = negativeColor; } else { color1 = positiveColor; color2 = Color.Lerp(negativeColor, positiveColor, (positiveEffect - negativeEffect) / positiveEffect); } } if (Character.IsDead) { color1 = Color.Lerp(color1, Color.Black, 0.75f); color2 = Color.Lerp(color2, Color.Black, 0.75f); } GameMain.GameScreen.GradientEffect.Parameters["color1"].SetValue(color1.ToVector4()); GameMain.GameScreen.GradientEffect.Parameters["color2"].SetValue(color2.ToVector4()); GameMain.GameScreen.GradientEffect.Parameters["midPoint"].SetValue(midPoint); GameMain.GameScreen.GradientEffect.Parameters["fadeDist"].SetValue(fadeDist); float scale = Math.Min(drawArea.Width / (float)limbHealth.IndicatorSprite.SourceRect.Width, drawArea.Height / (float)limbHealth.IndicatorSprite.SourceRect.Height); limbHealth.IndicatorSprite.Draw(spriteBatch, drawArea.Center.ToVector2(), Color.White, limbHealth.IndicatorSprite.Origin, 0, scale); if (GameMain.DebugDraw) { Rectangle highlightArea = GetLimbHighlightArea(limbHealth, drawArea); GUI.DrawRectangle(spriteBatch, highlightArea, Color.Red, false); GUI.DrawRectangle(spriteBatch, drawArea, Color.Red, false); } i++; } spriteBatch.End(); spriteBatch.Begin(SpriteSortMode.Deferred, Lights.CustomBlendStates.Multiplicative); if (limbIndicatorOverlay != null) { float overlayScale = Math.Min( drawArea.Width / (float)limbIndicatorOverlay.FrameSize.X, drawArea.Height / (float)limbIndicatorOverlay.FrameSize.Y); int frame; 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); } if (allowHighlight) { i = 0; foreach (LimbHealth limbHealth in limbHealths) { if (limbHealth.HighlightSprite == null) { continue; } float scale = Math.Min(drawArea.Width / (float)limbHealth.HighlightSprite.SourceRect.Width, drawArea.Height / (float)limbHealth.HighlightSprite.SourceRect.Height); int drawCount = 0; if (i == highlightedLimbIndex) { drawCount++; } if (i == selectedLimbIndex) { drawCount++; } for (int j = 0; j < drawCount; j++) { limbHealth.HighlightSprite.Draw(spriteBatch, drawArea.Center.ToVector2(), Color.White, limbHealth.HighlightSprite.Origin, 0, scale); } i++; } } spriteBatch.End(); spriteBatch.Begin(SpriteSortMode.Deferred, blendState: BlendState.NonPremultiplied, rasterizerState: GameMain.ScissorTestEnable); i = 0; foreach (LimbHealth limbHealth in limbHealths) { afflictionsDisplayedOnLimb.Clear(); foreach (var affliction in afflictions) { if (ShouldDisplayAfflictionOnLimb(affliction, limbHealth)) { afflictionsDisplayedOnLimb.Add(affliction.Key); } } if (!afflictionsDisplayedOnLimb.Any()) { i++; continue; } 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); float iconScale = 0.25f * scale; Vector2 iconPos = highlightArea.Center.ToVector2(); //Affliction mostSevereAffliction = thisAfflictions.FirstOrDefault(a => !a.Prefab.IsBuff && !thisAfflictions.Any(a2 => !a2.Prefab.IsBuff && a2.Strength > a.Strength)) ?? thisAfflictions.FirstOrDefault(); Affliction mostSevereAffliction = SortAfflictionsBySeverity(afflictionsDisplayedOnLimb, excludeBuffs: false).FirstOrDefault(); if (mostSevereAffliction != null) { DrawLimbAfflictionIcon(spriteBatch, mostSevereAffliction, iconScale, ref iconPos); } if (afflictionsDisplayedOnLimb.Count() > 1) { string additionalAfflictionCount = $"+{afflictionsDisplayedOnLimb.Count() - 1}"; Vector2 displace = GUIStyle.SubHeadingFont.MeasureString(additionalAfflictionCount); GUIStyle.SubHeadingFont.DrawString(spriteBatch, additionalAfflictionCount, iconPos + new Vector2(displace.X * 1.1f, -displace.Y * 0.45f), Color.Black * 0.75f); GUIStyle.SubHeadingFont.DrawString(spriteBatch, additionalAfflictionCount, iconPos + new Vector2(displace.X, -displace.Y * 0.5f), Color.White); } i++; } if (selectedLimbIndex > -1 && afflictionIconList.Content.CountChildren > 0) { LimbHealth limbHealth = limbHealths[selectedLimbIndex]; if (limbHealth?.IndicatorSprite != null) { Rectangle selectedLimbArea = GetLimbHighlightArea(limbHealth, drawArea); GUI.DrawLine(spriteBatch, new Vector2(afflictionIconList.Rect.X, afflictionIconList.Rect.Y), selectedLimbArea.Center.ToVector2(), Color.LightGray * 0.5f, width: 4); } } } private bool ShouldDisplayAfflictionOnLimb(KeyValuePair kvp, LimbHealth limbHealth) { if (!kvp.Key.ShouldShowIcon(Character)) { return false; } if (kvp.Value == limbHealth) { return true; } else if (kvp.Value == null) { Limb indicatorLimb = Character.AnimController.GetLimb(kvp.Key.Prefab.IndicatorLimb); return indicatorLimb != null && indicatorLimb.HealthIndex == limbHealths.IndexOf(limbHealth); } return false; } private void DrawLimbAfflictionIcon(SpriteBatch spriteBatch, Affliction affliction, float iconScale, ref Vector2 iconPos) { if (!affliction.ShouldShowIcon(Character) || affliction.Prefab.Icon == null) { return; } Vector2 iconSize = affliction.Prefab.Icon.size * iconScale; float showIconThreshold = Character.Controlled?.CharacterHealth == this ? affliction.Prefab.ShowIconThreshold : affliction.Prefab.ShowIconToOthersThreshold; //afflictions that have a strength of less than 10 are faded out slightly float alpha = MathHelper.Lerp(0.3f, 1.0f, (affliction.Strength - showIconThreshold) / Math.Min(affliction.Prefab.MaxStrength - showIconThreshold, 10.0f)); affliction.Prefab.Icon.Draw(spriteBatch, iconPos - iconSize / 2.0f, GetAfflictionIconColor(affliction) * 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.SourceRect.Width / 2 - limbHealth.HighlightArea.X) * scale), (int)(drawArea.Center.Y - (limbHealth.IndicatorSprite.SourceRect.Height / 2 - limbHealth.HighlightArea.Y) * scale), (int)(limbHealth.HighlightArea.Width * scale), (int)(limbHealth.HighlightArea.Height * scale)); } public void SetHealthBarVisibility(bool value) { healthBarHolder.Visible = value; } private readonly List<(LimbHealth limb, AfflictionPrefab afflictionPrefab, float strength)> newAfflictions = new List<(LimbHealth limb, AfflictionPrefab afflictionPrefab, float strength)>(); private readonly List<(AfflictionPrefab.PeriodicEffect effect, float timer)> newPeriodicEffects = new List<(AfflictionPrefab.PeriodicEffect effect, float timer)>(); public void ClientRead(IReadMessage inc) { newAfflictions.Clear(); newPeriodicEffects.Clear(); bool newAdded = false; byte afflictionCount = inc.ReadByte(); for (int i = 0; i < afflictionCount; i++) { uint afflictionID = inc.ReadUInt32(); AfflictionPrefab afflictionPrefab = AfflictionPrefab.Prefabs.Find(p => p.UintIdentifier == afflictionID); if (afflictionPrefab == null) { DebugConsole.ThrowError("Error while reading character health data: affliction with the uint ID " + afflictionID + " not found."); //read the 8 bytes for affliction strength anyway to prevent messing up reading rest of the message _ = inc.ReadRangedSingle(0.0f, 100.0f, 8); int _periodicAfflictionCount = inc.ReadByte(); for (int j = 0; j < _periodicAfflictionCount; j++) { _ = inc.ReadByte(); } continue; } float afflictionStrength = inc.ReadRangedSingle(0.0f, afflictionPrefab.MaxStrength, 8); int periodicAfflictionCount = inc.ReadByte(); for (int j = 0; j < periodicAfflictionCount; j++) { float periodicAfflictionTimer = inc.ReadRangedSingle(0, afflictionPrefab.PeriodicEffects[j].MaxInterval, 8); newPeriodicEffects.Add((afflictionPrefab.PeriodicEffects[j], periodicAfflictionTimer)); } newAfflictions.Add((null, afflictionPrefab, afflictionStrength)); } byte limbAfflictionCount = inc.ReadByte(); for (int i = 0; i < limbAfflictionCount; i++) { int limbIndex = inc.ReadRangedInteger(0, limbHealths.Count - 1); uint afflictionID = inc.ReadUInt32(); AfflictionPrefab afflictionPrefab = AfflictionPrefab.Prefabs.Find(p => p.UintIdentifier == afflictionID); if (afflictionPrefab == null) { DebugConsole.ThrowError("Error while reading character health data: affliction with the uint ID " + afflictionID + " not found."); //read the 8 bytes for affliction strength anyway to prevent messing up reading rest of the message _ = inc.ReadRangedSingle(0.0f, 100.0f, 8); int _periodicAfflictionCount = inc.ReadByte(); for (int j = 0; j < _periodicAfflictionCount; j++) { _ = inc.ReadByte(); } continue; } float afflictionStrength = inc.ReadRangedSingle(0.0f, afflictionPrefab.MaxStrength, 8); int periodicAfflictionCount = inc.ReadByte(); for (int j = 0; j < periodicAfflictionCount; j++) { float periodicAfflictionTimer = inc.ReadRangedSingle(afflictionPrefab.PeriodicEffects[j].MinInterval, afflictionPrefab.PeriodicEffects[j].MaxInterval, 8); newPeriodicEffects.Add((afflictionPrefab.PeriodicEffects[j], periodicAfflictionTimer)); } newAfflictions.Add((limbHealths[limbIndex], afflictionPrefab, afflictionStrength)); } foreach ((Affliction affliction, LimbHealth limbHealth) in afflictions) { //deactivate afflictions that weren't included in the network message if (newAfflictions.None(a => affliction.Prefab == a.afflictionPrefab && limbHealth == a.limb)) { affliction.Strength = 0.0f; } } foreach (var (limb, afflictionPrefab, strength) in newAfflictions) { Affliction existingAffliction = null; foreach ((Affliction affliction, LimbHealth limbHealth) in afflictions) { if (affliction.Prefab == afflictionPrefab && limbHealth == limb) { existingAffliction = affliction; break; } } if (existingAffliction == null) { existingAffliction = afflictionPrefab.Instantiate(strength); afflictions.Add(existingAffliction, limb); newAdded = true; } existingAffliction.SetStrength(strength); if (existingAffliction == stunAffliction) { Character.SetStun(existingAffliction.Strength, true, true); } foreach (var periodicEffect in newPeriodicEffects) { if (!existingAffliction.Prefab.PeriodicEffects.Contains(periodicEffect.effect)) { continue; } if (existingAffliction.Strength < periodicEffect.effect.MinStrength) { continue; } if (periodicEffect.effect.MaxStrength > 0 && existingAffliction.Strength > periodicEffect.effect.MaxStrength) { continue; } //timer has wrapped around, apply the effect if (periodicEffect.timer - existingAffliction.PeriodicEffectTimers[periodicEffect.effect] > periodicEffect.effect.MinInterval / 2) { foreach (StatusEffect effect in periodicEffect.effect.StatusEffects) { Limb targetLimb = Character.AnimController.Limbs.FirstOrDefault(l => l.HealthIndex == limbHealths.IndexOf(limb)); existingAffliction.ApplyStatusEffect(ActionType.OnActive, effect, deltaTime: 1.0f, this, targetLimb: targetLimb); } } existingAffliction.PeriodicEffectTimers[periodicEffect.effect] = periodicEffect.timer; } } CalculateVitality(); DisplayedVitality = Vitality; if (newAdded) { MedicalClinic.OnAfflictionCountChanged(Character); } } partial void UpdateSkinTint() { FaceTint = DefaultFaceTint; BodyTint = Color.TransparentBlack; if (!Character.Params.Health.ApplyAfflictionColors) { return; } foreach ((Affliction affliction, LimbHealth _) in afflictions) { Color faceTint = affliction.GetFaceTint(); if (faceTint.A > FaceTint.A) { FaceTint = faceTint; } Color bodyTint = affliction.GetBodyTint(); if (bodyTint.A > BodyTint.A) { BodyTint = bodyTint; } } } partial void UpdateLimbAfflictionOverlays() { foreach (Limb limb in Character.AnimController.Limbs) { limb.BurnOverlayStrength = 0.0f; limb.DamageOverlayStrength = 0.0f; } foreach ((Affliction affliction, LimbHealth limbHealth) in afflictions) { if (affliction.Prefab.BurnOverlayAlpha <= 0.0f && affliction.Prefab.DamageOverlayAlpha <= 0.0f) { continue; } float burnStrength = affliction.Strength / Math.Min(affliction.Prefab.MaxStrength, 100) * affliction.Prefab.BurnOverlayAlpha; float damageOverlayStrength = affliction.Strength / Math.Min(affliction.Prefab.MaxStrength, 100) * affliction.Prefab.DamageOverlayAlpha; foreach (Limb limb in Character.AnimController.Limbs) { if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count) { continue; } if (limbHealth == limbHealths[limb.HealthIndex] || !affliction.Prefab.LimbSpecific) { limb.BurnOverlayStrength += burnStrength; limb.DamageOverlayStrength += damageOverlayStrength; } else { limb.BurnOverlayStrength += burnStrength / 2; } } } } partial void RemoveProjSpecific() { foreach (LimbHealth limbHealth in limbHealths) { if (limbHealth.IndicatorSprite != null) { limbHealth.IndicatorSprite.Remove(); limbHealth.IndicatorSprite = null; } } medUIExtra?.Remove(); medUIExtra = null; Character.OnAttacked -= OnAttacked; limbIndicatorOverlay?.Remove(); limbIndicatorOverlay = null; if (healthWindow != null) { healthWindow.RectTransform.Parent = null; healthWindow = null; } if (healthBarHolder != null) { healthBarHolder.RectTransform.Parent = null; healthBarHolder = null; } if (SuicideButton != null) { SuicideButton.RectTransform.Parent = null; SuicideButton = null; } if (afflictionTooltip != null) { afflictionTooltip.RectTransform.Parent = null; afflictionTooltip = null; } } } }