using System; using TMPro; using UnityEngine; using UnityEngine.UI; namespace RoR2.UI; [RequireComponent(typeof(RectTransform))] [RequireComponent(typeof(Canvas))] public class HealthBar : MonoBehaviour { public struct BarInfo { public bool enabled; public Color color; public Sprite sprite; public Image.Type imageType; public int index; public float normalizedXMin; public float normalizedXMax; public float sizeDelta; public bool DimsEqual(BarInfo rhs) { if (normalizedXMin == rhs.normalizedXMin && normalizedXMax == rhs.normalizedXMax) { return sizeDelta == rhs.sizeDelta; } return false; } public void SetDims(BarInfo rhs) { normalizedXMin = rhs.normalizedXMin; normalizedXMax = rhs.normalizedXMax; sizeDelta = rhs.sizeDelta; } } private struct BarInfoCollection { public BarInfo trailingUnderHealthbarInfo; public BarInfo instantHealthbarInfo; public BarInfo trailingOverHealthbarInfo; public BarInfo shieldBarInfo; public BarInfo curseBarInfo; public BarInfo barrierBarInfo; public BarInfo cullBarInfo; public BarInfo flashBarInfo; public BarInfo magneticBarInfo; public BarInfo ospBarInfo; public BarInfo lowHealthOverBarInfo; public BarInfo lowHealthUnderBarInfo; public void Init() { trailingUnderHealthbarInfo.index = -1; instantHealthbarInfo.index = -1; trailingOverHealthbarInfo.index = -1; shieldBarInfo.index = -1; curseBarInfo.index = -1; barrierBarInfo.index = -1; cullBarInfo.index = -1; flashBarInfo.index = -1; magneticBarInfo.index = -1; ospBarInfo.index = -1; lowHealthOverBarInfo.index = -1; lowHealthUnderBarInfo.index = -1; } public int GetActiveCount() { int count = 0; Check(ref lowHealthUnderBarInfo); Check(ref trailingUnderHealthbarInfo); Check(ref instantHealthbarInfo); Check(ref trailingOverHealthbarInfo); Check(ref shieldBarInfo); Check(ref curseBarInfo); Check(ref barrierBarInfo); Check(ref flashBarInfo); Check(ref cullBarInfo); Check(ref magneticBarInfo); Check(ref ospBarInfo); Check(ref lowHealthOverBarInfo); return count; void Check(ref BarInfo field) { count += (field.enabled ? 1 : 0); } } } private HealthComponent _source; private HealthComponent oldSource; public Material baseSubBarMat; public HealthBarStyle style; [Tooltip("The container rect for the actual bars.")] public RectTransform barContainer; public RectTransform eliteBackdropRectTransform; public Image criticallyHurtImage; public Image deadImage; public float maxLastHitTimer = 1f; public bool scaleHealthbarWidth; public float minHealthbarWidth; public float maxHealthbarWidth; public float minHealthbarHealth; public float maxHealthbarHealth; private float displayStringCurrentHealth; private float displayStringFullHealth; private RectTransform rectTransform; private float cachedFractionalValue = 1f; private float healthFractionVelocity; private bool healthCritical; private bool isInventoryCheckDirty = true; private bool hasLowHealthItem; private bool hasLowHealthBuff; public float updateDelay = 0.1f; private float updateTimer; [NonSerialized] public CharacterBody viewerBody; private static readonly Color infusionPanelColor = new Color32(231, 84, 58, byte.MaxValue); private static readonly Color voidPanelColor = new Color32(217, 123, byte.MaxValue, byte.MaxValue); private static readonly Color voidShieldsColor = new Color32(byte.MaxValue, 57, 199, byte.MaxValue); private float theta; private UIElementPairAllocator barAllocator; private BarInfoCollection barInfoCollection; private float oldHealth; private float oldFullHealth; private bool oldHealthCritical; private bool oldAlive = true; public TextMeshProUGUI currentHealthText; public TextMeshProUGUI fullHealthText; public SpriteAsNumberManager spriteAsNumberManager; public HealthComponent source { get { return _source; } set { if (_source != value) { RemoveInventoryChangedHandler(); _source = value; healthFractionVelocity = 0f; cachedFractionalValue = (_source ? (_source.health / _source.fullCombinedHealth) : 0f); AddInventoryChangedHandler(); isInventoryCheckDirty = true; } } } private void Awake() { updateDelay = 0f; rectTransform = (RectTransform)base.transform; barAllocator = new UIElementPairAllocator(barContainer, style.barPrefab); } private void Start() { barInfoCollection.Init(); UpdateHealthbar(0f); } public void Update() { updateTimer -= Time.deltaTime; if (_source != oldSource) { updateTimer = 0f; oldSource = _source; } if (updateTimer <= 0f) { UpdateHealthbar(updateDelay - updateTimer); updateTimer = updateDelay; } } private void OnEnable() { AddInventoryChangedHandler(); isInventoryCheckDirty = true; } private void OnDisable() { RemoveInventoryChangedHandler(); } private void SetRectPosition(RectTransform rectTransform, float xMin, float xMax, float sizeDelta) { rectTransform.anchorMin = new Vector2(xMin, 0f); rectTransform.anchorMax = new Vector2(xMax, 1f); rectTransform.anchoredPosition = Vector2.zero; rectTransform.sizeDelta = new Vector2(sizeDelta * 0.5f + 1f, sizeDelta + 1f); } public void UpdateBarRects() { int i = 0; barAllocator.AllocateElements(barInfoCollection.GetActiveCount()); HandleBar(ref barInfoCollection.lowHealthUnderBarInfo); HandleBar(ref barInfoCollection.trailingUnderHealthbarInfo); HandleBar(ref barInfoCollection.instantHealthbarInfo); HandleBar(ref barInfoCollection.trailingOverHealthbarInfo); HandleBar(ref barInfoCollection.shieldBarInfo); HandleBar(ref barInfoCollection.curseBarInfo); HandleBar(ref barInfoCollection.barrierBarInfo); HandleBar(ref barInfoCollection.flashBarInfo); HandleBar(ref barInfoCollection.cullBarInfo); HandleBar(ref barInfoCollection.magneticBarInfo); HandleBar(ref barInfoCollection.ospBarInfo); HandleBar(ref barInfoCollection.lowHealthOverBarInfo); void HandleBar(ref BarInfo barInfo) { if (barInfo.enabled) { barInfo.index = i; Image image = barAllocator.elements[i]; BarInfo barInfo2 = barAllocator.elementsData[i]; if (!barInfo2.DimsEqual(barInfo)) { barInfo2.SetDims(barInfo); SetRectPosition((RectTransform)image.transform, barInfo.normalizedXMin, barInfo.normalizedXMax, barInfo.sizeDelta); } int num = i + 1; i = num; } } } private void ApplyBars() { int i = 0; barAllocator.AllocateElements(barInfoCollection.GetActiveCount()); HandleBar(ref barInfoCollection.lowHealthUnderBarInfo); HandleBar(ref barInfoCollection.trailingUnderHealthbarInfo); HandleBar(ref barInfoCollection.instantHealthbarInfo); HandleBar(ref barInfoCollection.trailingOverHealthbarInfo); HandleBar(ref barInfoCollection.shieldBarInfo); HandleBar(ref barInfoCollection.curseBarInfo); HandleBar(ref barInfoCollection.barrierBarInfo); HandleBar(ref barInfoCollection.flashBarInfo); HandleBar(ref barInfoCollection.cullBarInfo); HandleBar(ref barInfoCollection.magneticBarInfo); HandleBar(ref barInfoCollection.ospBarInfo); HandleBar(ref barInfoCollection.lowHealthOverBarInfo); void HandleBar(ref BarInfo barInfo) { if (barInfo.enabled) { Image image = barAllocator.elements[i]; BarInfo val = barAllocator.elementsData[i]; if (!val.DimsEqual(barInfo)) { val.SetDims(barInfo); SetRectPosition((RectTransform)image.transform, barInfo.normalizedXMin, barInfo.normalizedXMax, barInfo.sizeDelta); } if (barInfo.imageType != val.imageType) { val.imageType = barInfo.imageType; image.type = barInfo.imageType; } if (barInfo.sprite != val.sprite) { val.sprite = barInfo.sprite; image.sprite = barInfo.sprite; } if (barInfo.color != image.color) { val.color = barInfo.color; image.color = barInfo.color; } if (barInfo.normalizedXMin != val.normalizedXMin) { val.normalizedXMin = barInfo.normalizedXMin; } if (barInfo.normalizedXMax != val.normalizedXMax) { val.normalizedXMax = barInfo.normalizedXMax; } barAllocator.SetData(val, i); int num = i + 1; i = num; } } } private void UpdateBarInfos() { float currentBarEnd; if ((bool)source) { if (isInventoryCheckDirty) { CheckInventory(); } if ((bool)source && (bool)source.body) { hasLowHealthBuff = source.body.HasBuff(DLC2Content.Buffs.RevitalizeBuff); } HealthComponent.HealthBarValues healthBarValues = source.GetHealthBarValues(); currentBarEnd = 0f; _ = source.fullCombinedHealth; ref BarInfo lowHealthUnderBarInfo = ref barInfoCollection.lowHealthUnderBarInfo; lowHealthUnderBarInfo.enabled = (hasLowHealthItem || hasLowHealthBuff) && source.isHealthLow; lowHealthUnderBarInfo.normalizedXMin = 0f; lowHealthUnderBarInfo.normalizedXMax = HealthComponent.lowHealthFraction * (1f - healthBarValues.curseFraction); ApplyStyle(ref lowHealthUnderBarInfo, ref style.lowHealthUnderStyle); cachedFractionalValue = Mathf.SmoothDamp(cachedFractionalValue, healthBarValues.healthFraction, ref healthFractionVelocity, 0.2f, float.PositiveInfinity, Time.deltaTime); ref BarInfo trailingUnderHealthbarInfo = ref barInfoCollection.trailingUnderHealthbarInfo; trailingUnderHealthbarInfo.normalizedXMin = 0f; trailingUnderHealthbarInfo.normalizedXMax = Mathf.Max(cachedFractionalValue, healthBarValues.healthFraction); trailingUnderHealthbarInfo.enabled = !trailingUnderHealthbarInfo.normalizedXMax.Equals(trailingUnderHealthbarInfo.normalizedXMin); ApplyStyle(ref trailingUnderHealthbarInfo, ref style.trailingUnderHealthBarStyle); ref BarInfo instantHealthbarInfo = ref barInfoCollection.instantHealthbarInfo; instantHealthbarInfo.enabled = healthBarValues.healthFraction > 0f; ApplyStyle(ref instantHealthbarInfo, ref style.instantHealthBarStyle); AddBar(ref instantHealthbarInfo, healthBarValues.healthFraction); ref BarInfo trailingOverHealthbarInfo = ref barInfoCollection.trailingOverHealthbarInfo; trailingOverHealthbarInfo.normalizedXMin = 0f; trailingOverHealthbarInfo.normalizedXMax = Mathf.Min(cachedFractionalValue + 0.01f, healthBarValues.healthFraction); trailingOverHealthbarInfo.enabled = !trailingOverHealthbarInfo.normalizedXMax.Equals(trailingOverHealthbarInfo.normalizedXMin); ApplyStyle(ref trailingOverHealthbarInfo, ref style.trailingOverHealthBarStyle); if (healthBarValues.isVoid) { trailingOverHealthbarInfo.color = voidPanelColor; } if (healthBarValues.isBoss || healthBarValues.hasInfusion) { trailingOverHealthbarInfo.color = infusionPanelColor; } if (healthCritical && style.flashOnHealthCritical) { trailingOverHealthbarInfo.color = GetCriticallyHurtColor(); } ref BarInfo shieldBarInfo = ref barInfoCollection.shieldBarInfo; shieldBarInfo.enabled = healthBarValues.shieldFraction > 0f; ApplyStyle(ref shieldBarInfo, ref style.shieldBarStyle); if (healthBarValues.hasVoidShields) { shieldBarInfo.color = voidShieldsColor; } AddBar(ref shieldBarInfo, healthBarValues.shieldFraction); ref BarInfo curseBarInfo = ref barInfoCollection.curseBarInfo; curseBarInfo.enabled = healthBarValues.curseFraction > 0f; ApplyStyle(ref curseBarInfo, ref style.curseBarStyle); curseBarInfo.normalizedXMin = 1f - healthBarValues.curseFraction; curseBarInfo.normalizedXMax = 1f; ref BarInfo barrierBarInfo = ref barInfoCollection.barrierBarInfo; barrierBarInfo.enabled = source.barrier > 0f; ApplyStyle(ref barrierBarInfo, ref style.barrierBarStyle); barrierBarInfo.normalizedXMin = 0f; barrierBarInfo.normalizedXMax = healthBarValues.barrierFraction; ref BarInfo magneticBarInfo = ref barInfoCollection.magneticBarInfo; magneticBarInfo.enabled = source.magnetiCharge > 0f; magneticBarInfo.normalizedXMin = 0f; magneticBarInfo.normalizedXMax = healthBarValues.magneticFraction; magneticBarInfo.color = new Color(75f, 0f, 130f); ApplyStyle(ref magneticBarInfo, ref style.magneticStyle); float num = healthBarValues.cullFraction; if (healthBarValues.isElite && (bool)viewerBody) { num = Mathf.Max(num, viewerBody.executeEliteHealthFraction); } ref BarInfo cullBarInfo = ref barInfoCollection.cullBarInfo; cullBarInfo.enabled = num > 0f; cullBarInfo.normalizedXMin = 0f; cullBarInfo.normalizedXMax = num; ApplyStyle(ref cullBarInfo, ref style.cullBarStyle); float ospFraction = healthBarValues.ospFraction; ref BarInfo ospBarInfo = ref barInfoCollection.ospBarInfo; ospBarInfo.enabled = ospFraction > 0f; ospBarInfo.normalizedXMin = 0f; ospBarInfo.normalizedXMax = ospFraction; ApplyStyle(ref ospBarInfo, ref style.ospStyle); ref BarInfo lowHealthOverBarInfo = ref barInfoCollection.lowHealthOverBarInfo; lowHealthOverBarInfo.enabled = (hasLowHealthItem || hasLowHealthBuff) && !source.isHealthLow; lowHealthOverBarInfo.normalizedXMin = HealthComponent.lowHealthFraction * (1f - healthBarValues.curseFraction); lowHealthOverBarInfo.normalizedXMax = HealthComponent.lowHealthFraction * (1f - healthBarValues.curseFraction) + 0.005f; ApplyStyle(ref lowHealthOverBarInfo, ref style.lowHealthOverStyle); } void AddBar(ref BarInfo barInfo, float fraction) { if (barInfo.enabled) { barInfo.normalizedXMin = currentBarEnd; currentBarEnd = (barInfo.normalizedXMax = barInfo.normalizedXMin + fraction); } } static void ApplyStyle(ref BarInfo barInfo, ref HealthBarStyle.BarStyle barStyle) { barInfo.enabled &= barStyle.enabled; barInfo.color = barStyle.baseColor; barInfo.sprite = barStyle.sprite; barInfo.imageType = barStyle.imageType; barInfo.sizeDelta = barStyle.sizeDelta; } } public void InitializeHealthBar() { float num = 1f; if (!source) { return; } CharacterBody body = source.body; bool isElite = body.isElite; float fullHealth = source.fullHealth; if ((bool)eliteBackdropRectTransform) { if (isElite) { num += 1f; } eliteBackdropRectTransform.gameObject.SetActive(isElite); } if (scaleHealthbarWidth && (bool)body) { float x = Util.Remap(Mathf.Clamp((body.baseMaxHealth + body.baseMaxShield) * num, 0f, maxHealthbarHealth), minHealthbarHealth, maxHealthbarHealth, minHealthbarWidth, maxHealthbarWidth); rectTransform.sizeDelta = new Vector2(x, rectTransform.sizeDelta.y); } if ((bool)spriteAsNumberManager) { float num2 = Mathf.Ceil(source.combinedHealth); float num3 = Mathf.Ceil(fullHealth); if (num2 != displayStringCurrentHealth || num3 != displayStringFullHealth) { displayStringFullHealth = num3; displayStringCurrentHealth = num2; spriteAsNumberManager.SetHitPointValues((int)displayStringCurrentHealth, (int)displayStringFullHealth); } } else { if ((bool)currentHealthText) { float num4 = Mathf.Ceil(source.combinedHealth); if (num4 != displayStringCurrentHealth) { displayStringCurrentHealth = num4; currentHealthText.text = num4.ToString(); } } if ((bool)fullHealthText) { float num5 = Mathf.Ceil(fullHealth); if (num5 != displayStringFullHealth) { displayStringFullHealth = num5; fullHealthText.text = num5.ToString(); } } } healthCritical = source.isHealthLow && source.alive; if ((bool)criticallyHurtImage) { if (healthCritical) { criticallyHurtImage.enabled = true; criticallyHurtImage.color = GetCriticallyHurtColor(); } else { criticallyHurtImage.enabled = false; } } if ((bool)deadImage) { deadImage.enabled = !source.alive; } } public void UpdateHealthbar(float deltaTime) { float num = 1f; if ((bool)source) { _ = source.body.isElite; float fullHealth = source.fullHealth; float num2 = source.fullHealth + source.fullShield; Mathf.Clamp01(source.health / num2 * num); Mathf.Clamp01(source.shield / num2 * num); if ((bool)spriteAsNumberManager) { float num3 = Mathf.Ceil(source.combinedHealth); float num4 = Mathf.Ceil(fullHealth); if (num3 != displayStringCurrentHealth || num4 != displayStringFullHealth) { displayStringFullHealth = num4; displayStringCurrentHealth = num3; spriteAsNumberManager.SetHitPointValues((int)displayStringCurrentHealth, (int)displayStringFullHealth); } } else { if ((bool)currentHealthText && source.combinedHealth != oldHealth) { oldHealth = source.combinedHealth; float num5 = Mathf.Ceil(source.combinedHealth); if (num5 != displayStringCurrentHealth) { displayStringCurrentHealth = num5; currentHealthText.text = num5.ToString(); } } if ((bool)fullHealthText && fullHealth != oldFullHealth) { oldFullHealth = fullHealth; float num6 = Mathf.Ceil(fullHealth); if (num6 != displayStringFullHealth) { displayStringFullHealth = num6; fullHealthText.text = num6.ToString(); } } } healthCritical = source.isHealthLow && source.alive; if ((bool)criticallyHurtImage && healthCritical != oldHealthCritical) { oldHealthCritical = healthCritical; if (healthCritical) { criticallyHurtImage.enabled = true; criticallyHurtImage.color = GetCriticallyHurtColor(); } else { criticallyHurtImage.enabled = false; } } if ((bool)deadImage && source.alive != oldAlive) { oldAlive = source.alive; deadImage.enabled = !source.alive; } } else if ((bool)deadImage && oldAlive) { oldAlive = false; deadImage.enabled = true; } UpdateBarInfos(); ApplyBars(); } private void AddInventoryChangedHandler() { if ((bool)source?.body?.inventory) { source.body.inventory.onInventoryChanged += OnInventoryChanged; } } private void RemoveInventoryChangedHandler() { if ((bool)source?.body?.inventory) { source.body.inventory.onInventoryChanged -= OnInventoryChanged; } } private void OnInventoryChanged() { isInventoryCheckDirty = true; } private void CheckInventory() { isInventoryCheckDirty = false; Inventory inventory = source?.body?.inventory; if (!inventory) { return; } hasLowHealthItem = false; foreach (ItemIndex item in ItemCatalog.GetItemsWithTag(ItemTag.LowHealth)) { if (inventory.GetItemCount(item) > 0) { hasLowHealthItem = true; break; } } } public static Color GetCriticallyHurtColor() { if (Mathf.FloorToInt(Time.fixedTime * 10f) % 2 != 0) { return ColorCatalog.GetColor(ColorCatalog.ColorIndex.Teleporter); } return Color.white; } }