using System; using System.Collections.Generic; using System.Text; using UnityEngine; using UnityEngine.UI; using TMPro; using Colosseum.Abnormalities; using Colosseum.Enemy; namespace Colosseum.UI { /// /// 보스 체력바 UI 컴포넌트. /// BossEnemy의 체력 변화를 자동으로 UI에 반영합니다. /// public class BossHealthBarUI : MonoBehaviour { [Header("References")] [Tooltip("체력 슬라이더 (없으면 자동 검색)")] [SerializeField] private Slider healthSlider; [Tooltip("체력 텍스트 (예: '999 / 999')")] [SerializeField] private TMP_Text healthText; [Tooltip("보스 이름 텍스트")] [SerializeField] private TMP_Text bossNameText; [Tooltip("보스 이상상태 요약 텍스트 (비어 있으면 런타임에 자동 생성)")] [SerializeField] private TMP_Text abnormalitySummaryText; [Header("Target")] [Tooltip("추적할 보스 (런타임에 설정 가능)")] [SerializeField] private BossEnemy targetBoss; [Header("Settings")] [Tooltip("보스 사망 시 UI 숨김 여부")] [SerializeField] private bool hideOnDeath = true; [Tooltip("슬라이더 값 변환 속도")] [Min(0f)] [SerializeField] private float lerpSpeed = 5f; [Tooltip("보스 이상상태 요약 텍스트를 자동 생성할지 여부")] [SerializeField] private bool autoCreateAbnormalitySummary = true; [Header("Signature UI")] [Tooltip("시그니처 패턴 전용 UI를 표시할지 여부")] [SerializeField] private bool showSignatureUi = false; [Tooltip("집행 개시 진행도를 표시할 루트 오브젝트")] [SerializeField] private RectTransform signatureRoot; [SerializeField] private Image signatureFillImage; [SerializeField] private TMP_Text signatureNameText; [SerializeField] private TMP_Text signatureDetailText; private float displayHealthRatio; private float targetHealthRatio; private bool isSubscribed; private bool isSubscribedToStaticEvent; private BossCombatBehaviorContext bossCombatContext; private AbnormalityManager targetAbnormalityManager; /// /// 현재 추적 중인 보스 /// public BossEnemy TargetBoss => targetBoss; public string CurrentAbnormalitySummary => abnormalitySummaryText != null ? abnormalitySummaryText.text : string.Empty; /// /// 보스 수동 설정 (런타임에서 호출) /// public void SetBoss(BossEnemy boss) { // 기존 보스 이벤트 구독 해제 UnsubscribeFromBoss(); targetBoss = boss; targetAbnormalityManager = targetBoss != null ? targetBoss.GetComponent() : null; // 새 보스 이벤트 구독 SubscribeToBoss(); // 초기 UI 업데이트 if (targetBoss != null) { bossCombatContext = targetBoss.GetComponent(); EnsureAbnormalitySummaryText(); UpdateBossName(); UpdateHealthImmediate(); UpdateAbnormalitySummary(); UpdateSignatureUi(); gameObject.SetActive(true); } else { bossCombatContext = null; if (abnormalitySummaryText != null) abnormalitySummaryText.text = string.Empty; SetSignatureVisible(false); gameObject.SetActive(false); } } /// /// 보스 스폰 이벤트 핸들러 /// private void OnBossSpawned(BossEnemy boss) { if (boss == null) return; SetBoss(boss); } private void Awake() { // 컴포넌트 자동 검색 if (healthSlider == null) healthSlider = GetComponentInChildren(); if (healthText == null) healthText = transform.Find("SliderBox/Label_HP")?.GetComponent(); if (bossNameText == null) bossNameText = transform.Find("SliderBox/Label_BossName")?.GetComponent(); if (showSignatureUi) { EnsureSignatureUi(); } EnsureAbnormalitySummaryText(); } private void OnEnable() { // 정적 이벤트 구독 (보스 스폰 자동 감지) if (!isSubscribedToStaticEvent) { BossEnemy.OnBossSpawned += OnBossSpawned; isSubscribedToStaticEvent = true; } } private void OnDisable() { // 정적 이벤트 구독 해제 if (isSubscribedToStaticEvent) { BossEnemy.OnBossSpawned -= OnBossSpawned; isSubscribedToStaticEvent = false; } } private void Start() { // 이미 활성화된 보스가 있으면 연결 if (BossEnemy.ActiveBoss != null) { SetBoss(BossEnemy.ActiveBoss); } // 인스펙터에서 설정된 보스가 있으면 구독 else if (targetBoss != null) { SubscribeToBoss(); bossCombatContext = targetBoss.GetComponent(); UpdateBossName(); UpdateHealthImmediate(); UpdateAbnormalitySummary(); UpdateSignatureUi(); } else { // 보스가 없으면 비활성화 (이벤트 대기) gameObject.SetActive(false); } } private void Update() { // 부드러운 체력바 애니메이션 if (!Mathf.Approximately(displayHealthRatio, targetHealthRatio)) { displayHealthRatio = Mathf.Lerp(displayHealthRatio, targetHealthRatio, lerpSpeed * Time.deltaTime); if (Mathf.Abs(displayHealthRatio - targetHealthRatio) < 0.01f) displayHealthRatio = targetHealthRatio; UpdateSliderVisual(); } UpdateSignatureUi(); if (targetBoss != null) { UpdateHealthText(targetBoss.CurrentHealth, targetBoss.MaxHealth); UpdateAbnormalitySummary(); } } private void OnDestroy() { UnsubscribeFromBoss(); // 정적 이벤트 구독 해제 if (isSubscribedToStaticEvent) { BossEnemy.OnBossSpawned -= OnBossSpawned; isSubscribedToStaticEvent = false; } } private void SubscribeToBoss() { if (targetBoss == null || isSubscribed) return; targetBoss.OnHealthChanged += OnBossHealthChanged; targetBoss.OnDeath += OnBossDeath; isSubscribed = true; } private void UnsubscribeFromBoss() { if (targetBoss == null || !isSubscribed) return; targetBoss.OnHealthChanged -= OnBossHealthChanged; targetBoss.OnDeath -= OnBossDeath; isSubscribed = false; } private void OnBossHealthChanged(float currentHealth, float maxHealth) { if (maxHealth <= 0f) return; targetHealthRatio = Mathf.Clamp01(currentHealth / maxHealth); UpdateHealthText(currentHealth, maxHealth); } private void OnBossDeath() { if (hideOnDeath) { gameObject.SetActive(false); } } private void UpdateHealthImmediate() { if (targetBoss == null) return; float currentHealth = targetBoss.CurrentHealth; float maxHealth = targetBoss.MaxHealth; if (maxHealth <= 0f) return; targetHealthRatio = Mathf.Clamp01(currentHealth / maxHealth); displayHealthRatio = targetHealthRatio; UpdateSliderVisual(); UpdateHealthText(currentHealth, maxHealth); } private void UpdateSliderVisual() { if (healthSlider != null) { healthSlider.value = displayHealthRatio; } } private void UpdateHealthText(float currentHealth, float maxHealth) { if (healthText != null) { int shieldValue = targetBoss != null ? Mathf.CeilToInt(targetBoss.Shield) : 0; healthText.text = shieldValue > 0 ? $"{Mathf.CeilToInt(currentHealth)} / {Mathf.CeilToInt(maxHealth)} (+{shieldValue})" : $"{Mathf.CeilToInt(currentHealth)} / {Mathf.CeilToInt(maxHealth)}"; } } private void UpdateBossName() { if (bossNameText == null || targetBoss == null) return; // EnemyData에서 보스 이름 가져오기 if (targetBoss.Data != null && !string.IsNullOrEmpty(targetBoss.Data.EnemyName)) { bossNameText.text = targetBoss.Data.EnemyName; } else { // 폴백: GameObject 이름 사용 bossNameText.text = targetBoss.name; } } private void EnsureAbnormalitySummaryText() { if (abnormalitySummaryText != null || !autoCreateAbnormalitySummary) return; Transform sliderBox = transform.Find("SliderBox"); if (sliderBox == null) sliderBox = transform; GameObject summaryObject = new GameObject("Label_BossAbnormalities", typeof(RectTransform), typeof(TextMeshProUGUI)); summaryObject.transform.SetParent(sliderBox, false); RectTransform rectTransform = summaryObject.GetComponent(); rectTransform.anchorMin = new Vector2(0f, 1f); rectTransform.anchorMax = new Vector2(1f, 1f); rectTransform.pivot = new Vector2(0.5f, 1f); rectTransform.anchoredPosition = new Vector2(0f, -32f); rectTransform.sizeDelta = new Vector2(0f, 24f); TextMeshProUGUI summaryText = summaryObject.GetComponent(); summaryText.fontSize = 16f; summaryText.alignment = TextAlignmentOptions.MidlineLeft; summaryText.textWrappingMode = TextWrappingModes.NoWrap; summaryText.richText = true; summaryText.text = string.Empty; if (bossNameText != null && bossNameText.font != null) { summaryText.font = bossNameText.font; summaryText.fontSharedMaterial = bossNameText.fontSharedMaterial; } else if (TMP_Settings.defaultFontAsset != null) { summaryText.font = TMP_Settings.defaultFontAsset; } abnormalitySummaryText = summaryText; } private void UpdateAbnormalitySummary() { if (abnormalitySummaryText == null) { EnsureAbnormalitySummaryText(); } if (abnormalitySummaryText == null) return; if (targetAbnormalityManager == null) { abnormalitySummaryText.text = string.Empty; return; } IReadOnlyList activeAbnormalities = targetAbnormalityManager.ActiveAbnormalities; StringBuilder builder = new StringBuilder(); for (int i = 0; i < activeAbnormalities.Count; i++) { ActiveAbnormality abnormality = activeAbnormalities[i]; if (abnormality?.Data == null || !abnormality.Data.showInUI) continue; if (builder.Length > 0) builder.Append(" "); string color = abnormality.Data.isDebuff ? "#FF7070" : "#70D0FF"; builder.Append(""); builder.Append(abnormality.Data.abnormalityName); if (!abnormality.Data.IsPermanent) { builder.Append(" "); builder.Append(Mathf.CeilToInt(Mathf.Max(0f, abnormality.RemainingDuration))); builder.Append("s"); } builder.Append(""); } abnormalitySummaryText.text = builder.ToString(); } private void UpdateSignatureUi() { if (!showSignatureUi) { SetSignatureVisible(false); return; } if (signatureRoot == null) return; if (targetBoss == null) { SetSignatureVisible(false); return; } if (bossCombatContext == null) bossCombatContext = targetBoss.GetComponent(); if (bossCombatContext == null || !bossCombatContext.IsSignaturePatternActive) { SetSignatureVisible(false); return; } SetSignatureVisible(true); if (signatureNameText != null) { signatureNameText.text = string.IsNullOrEmpty(bossCombatContext.SignaturePatternName) ? "시그니처" : bossCombatContext.SignaturePatternName; } if (signatureDetailText != null) { signatureDetailText.text = $"차단 {Mathf.CeilToInt(bossCombatContext.SignatureAccumulatedDamage)} / {Mathf.CeilToInt(bossCombatContext.SignatureRequiredDamage)}" + $" | {bossCombatContext.SignatureRemainingTime:0.0}s"; } if (signatureFillImage != null) { signatureFillImage.fillAmount = 1f - bossCombatContext.SignatureCastProgressNormalized; } } private void EnsureSignatureUi() { if (!showSignatureUi) return; if (signatureRoot != null && signatureFillImage != null && signatureNameText != null && signatureDetailText != null) return; Transform sliderBox = transform.Find("SliderBox"); if (sliderBox == null) sliderBox = transform; GameObject rootObject = new GameObject("SignatureBar", typeof(RectTransform), typeof(Image)); rootObject.transform.SetParent(sliderBox, false); signatureRoot = rootObject.GetComponent(); Image backgroundImage = rootObject.GetComponent(); backgroundImage.color = new Color(0.08f, 0.08f, 0.08f, 0.88f); signatureRoot.anchorMin = new Vector2(0f, 0f); signatureRoot.anchorMax = new Vector2(1f, 0f); signatureRoot.pivot = new Vector2(0.5f, 1f); signatureRoot.anchoredPosition = new Vector2(0f, -48f); signatureRoot.sizeDelta = new Vector2(0f, 42f); GameObject fillObject = new GameObject("Fill", typeof(RectTransform), typeof(Image)); fillObject.transform.SetParent(signatureRoot, false); RectTransform fillRect = fillObject.GetComponent(); signatureFillImage = fillObject.GetComponent(); signatureFillImage.color = new Color(0.88f, 0.48f, 0.12f, 0.95f); signatureFillImage.type = Image.Type.Filled; signatureFillImage.fillMethod = Image.FillMethod.Horizontal; signatureFillImage.fillOrigin = 0; signatureFillImage.fillAmount = 1f; fillRect.anchorMin = new Vector2(0f, 0f); fillRect.anchorMax = new Vector2(1f, 1f); fillRect.offsetMin = new Vector2(2f, 2f); fillRect.offsetMax = new Vector2(-2f, -2f); signatureNameText = CreateSignatureText("Label_SignatureName", TextAlignmentOptions.TopLeft, 18f, FontStyles.Bold); signatureDetailText = CreateSignatureText("Label_SignatureDetail", TextAlignmentOptions.TopRight, 15f, FontStyles.Normal); SetSignatureVisible(false); } private TMP_Text CreateSignatureText(string objectName, TextAlignmentOptions alignment, float fontSize, FontStyles fontStyle) { GameObject textObject = new GameObject(objectName, typeof(RectTransform), typeof(TextMeshProUGUI)); textObject.transform.SetParent(signatureRoot, false); RectTransform rectTransform = textObject.GetComponent(); rectTransform.anchorMin = new Vector2(0f, 0f); rectTransform.anchorMax = new Vector2(1f, 1f); rectTransform.offsetMin = new Vector2(6f, 4f); rectTransform.offsetMax = new Vector2(-6f, -4f); TMP_Text text = textObject.GetComponent(); text.alignment = alignment; text.fontSize = fontSize; text.fontStyle = fontStyle; text.color = Color.white; text.textWrappingMode = TextWrappingModes.NoWrap; if (bossNameText != null && bossNameText.font != null) { text.font = bossNameText.font; text.fontSharedMaterial = bossNameText.fontSharedMaterial; } return text; } private void SetSignatureVisible(bool visible) { if (signatureRoot == null || signatureRoot.gameObject.activeSelf == visible) return; signatureRoot.gameObject.SetActive(visible); } #if UNITY_EDITOR private void OnValidate() { if (healthSlider == null) healthSlider = GetComponentInChildren(); } #endif } }