feat: 보호막 타입 분리 및 드로그 시그니처 전조 정리
- 보호막을 단일 수치에서 타입별 독립 인스턴스 구조로 리팩터링하고 같은 타입만 갱신되도록 정리 - 플레이어/보스 보호막 상태를 이상상태와 연동해 HUD 및 보스 UI에서 타입별로 식별 가능하게 보강 - 드로그 집행 개시 전조를 집행 준비 이상상태 기반으로 재구성하고 관련 데이터와 보스 컨텍스트를 정리 - 전투 밸런스 계측기와 디버그 메뉴를 추가해 피해, 치유, 보호막, 위협, 패턴 사용량 측정 경로를 마련 - 테스트용 보호막 A/B와 시그니처 전조 자산을 추가하고 기본 포트 7777 원복 후 빌드 및 런타임 검증을 완료
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
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
|
||||
@@ -22,6 +25,9 @@ namespace Colosseum.UI
|
||||
|
||||
[Tooltip("보스 이름 텍스트")]
|
||||
[SerializeField] private TMP_Text bossNameText;
|
||||
|
||||
[Tooltip("보스 이상상태 요약 텍스트 (비어 있으면 런타임에 자동 생성)")]
|
||||
[SerializeField] private TMP_Text abnormalitySummaryText;
|
||||
|
||||
[Header("Target")]
|
||||
[Tooltip("추적할 보스 (런타임에 설정 가능)")]
|
||||
@@ -33,16 +39,31 @@ namespace Colosseum.UI
|
||||
|
||||
[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;
|
||||
|
||||
/// <summary>
|
||||
/// 현재 추적 중인 보스
|
||||
/// </summary>
|
||||
public BossEnemy TargetBoss => targetBoss;
|
||||
public string CurrentAbnormalitySummary => abnormalitySummaryText != null ? abnormalitySummaryText.text : string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 보스 수동 설정 (런타임에서 호출)
|
||||
@@ -53,6 +74,7 @@ namespace Colosseum.UI
|
||||
UnsubscribeFromBoss();
|
||||
|
||||
targetBoss = boss;
|
||||
targetAbnormalityManager = targetBoss != null ? targetBoss.GetComponent<AbnormalityManager>() : null;
|
||||
|
||||
// 새 보스 이벤트 구독
|
||||
SubscribeToBoss();
|
||||
@@ -60,12 +82,20 @@ namespace Colosseum.UI
|
||||
// 초기 UI 업데이트
|
||||
if (targetBoss != null)
|
||||
{
|
||||
bossCombatContext = targetBoss.GetComponent<BossCombatBehaviorContext>();
|
||||
EnsureAbnormalitySummaryText();
|
||||
UpdateBossName();
|
||||
UpdateHealthImmediate();
|
||||
UpdateAbnormalitySummary();
|
||||
UpdateSignatureUi();
|
||||
gameObject.SetActive(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
bossCombatContext = null;
|
||||
if (abnormalitySummaryText != null)
|
||||
abnormalitySummaryText.text = string.Empty;
|
||||
SetSignatureVisible(false);
|
||||
gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
@@ -93,6 +123,13 @@ namespace Colosseum.UI
|
||||
|
||||
if (bossNameText == null)
|
||||
bossNameText = transform.Find("SliderBox/Label_BossName")?.GetComponent<TMP_Text>();
|
||||
|
||||
if (showSignatureUi)
|
||||
{
|
||||
EnsureSignatureUi();
|
||||
}
|
||||
|
||||
EnsureAbnormalitySummaryText();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
@@ -124,8 +161,11 @@ namespace Colosseum.UI
|
||||
else if (targetBoss != null)
|
||||
{
|
||||
SubscribeToBoss();
|
||||
bossCombatContext = targetBoss.GetComponent<BossCombatBehaviorContext>();
|
||||
UpdateBossName();
|
||||
UpdateHealthImmediate();
|
||||
UpdateAbnormalitySummary();
|
||||
UpdateSignatureUi();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -145,6 +185,14 @@ namespace Colosseum.UI
|
||||
|
||||
UpdateSliderVisual();
|
||||
}
|
||||
|
||||
UpdateSignatureUi();
|
||||
|
||||
if (targetBoss != null)
|
||||
{
|
||||
UpdateHealthText(targetBoss.CurrentHealth, targetBoss.MaxHealth);
|
||||
UpdateAbnormalitySummary();
|
||||
}
|
||||
}
|
||||
private void OnDestroy()
|
||||
{
|
||||
@@ -218,7 +266,10 @@ namespace Colosseum.UI
|
||||
{
|
||||
if (healthText != null)
|
||||
{
|
||||
healthText.text = $"{Mathf.CeilToInt(currentHealth)} / {Mathf.CeilToInt(maxHealth)}";
|
||||
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()
|
||||
@@ -237,6 +288,221 @@ namespace Colosseum.UI
|
||||
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>();
|
||||
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<TextMeshProUGUI>();
|
||||
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<ActiveAbnormality> 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("<color=");
|
||||
builder.Append(color);
|
||||
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("</color>");
|
||||
}
|
||||
|
||||
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<BossCombatBehaviorContext>();
|
||||
|
||||
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<RectTransform>();
|
||||
Image backgroundImage = rootObject.GetComponent<Image>();
|
||||
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<RectTransform>();
|
||||
signatureFillImage = fillObject.GetComponent<Image>();
|
||||
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>();
|
||||
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<TextMeshProUGUI>();
|
||||
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()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user