Project_WL/Assets/Script/HUD/HUDDMGUI.cs

291 lines
14 KiB
C#

// WL-813g (#813) — 데미지 숫자 리듬화. 원본(힘민지) HUDDMGUI 에 표시 규칙만 얹는다.
// WL-813y (#813) — Q3 결함 D-6: 밝은 배경에서 숫자 대비 부족 → TMP 머티리얼 아웃라인 + 그림자.
// · uGUI Outline/Shadow(BaseMeshEffect)는 TMP 에 적용되지 않으므로 WLTextFxUtil.ApplyEdge 를 쓴다.
// · 변형 머티리얼은 (원본 머티리얼 + 값) 조합마다 **1장만** 캐시해 공유한다 = 배칭 유지 · 인스턴스 0.
// · 인스턴스당 1회만 적용(m_edgeDone)해 표시 경로(SetCore)에 GC 를 넣지 않는다.
// 기준서 v1 §B 요소 2: 일반 0.6 s · 크리 1.0 s · 크리 1.6배 · 합산 윈도우 0.25 s · 동시 상한(숫자죽 방지).
// 값은 전부 Assets/WL/UI/Settings/Resources/WL/WLCombatTextSettings.asset (C45 · 코드 상수 0).
// 🔴 C8 롤백: 에셋이 없거나 textEnabled/damageRhythmEnabled = false 면 **아래 원본 상수**(0.9 s · 배율 1 · 상한 없음)
// 로 그대로 돈다 — 호출부(Actor.cs:616/789/811/841)와 프리팹은 무수정이다.
using System.Collections.Generic;
using TMPro;
using UnityEngine;
public class HUDDMGUI : HUDBase
{
public TextMeshProUGUI[] dmgs;
// ── 원본 상수(롤백 기준값) — 설정 에셋이 없을 때 이 값으로 돈다
private float moveSpeed = 1.0f;
private float alphaSpeed = 0.5f;
private float destroyTime = .9f;
bool usedmg2;
int m_Index;
// ── WL-813g 상태(설정이 꺼져 있으면 전부 미사용)
static readonly List<HUDDMGUI> s_active = new List<HUDDMGUI>();
/// <summary>프로브가 읽는다 — 지금 화면에 떠 있는 숫자 수.</summary>
public static int ActiveCount { get { return s_active.Count; } }
/// <summary>프로브가 읽는다 — 상한 초과로 회수한 횟수 · 합산으로 흡수한 횟수.</summary>
public static int EvictedCount, MergedCount;
public static void ResetDiagnostics() { EvictedCount = MergedCount = 0; }
bool m_edgeDone; // WL-813y 아웃라인을 이 인스턴스에 이미 입혔는가
/// <summary>프로브가 읽는다 — 아웃라인을 입힌 인스턴스 수 · 마지막 실측 문자열.</summary>
public static int EdgeAppliedCount;
public static string LastEdgeDump = "";
bool m_wl; // 이번 표시에 813g 규칙이 적용됐는가
bool m_crit; // 크리티컬로 표시 중인가
float m_life, m_lifeMax; // 남은/전체 표시 시간(unscaled 아님 — 원본과 같은 Time.deltaTime 계열)
float m_rise, m_fadeStart, m_scale;
string m_color; // 합산 판정 키(같은 색끼리만 합친다 = 피해/회복/마나 섞임 방지)
double m_total; int m_hits;
/// <summary>이 인스턴스가 합산 중인 대상(합산 창이 살아 있는 동안만 유효).</summary>
public Transform MergeTarget { get { return m_MergeTarget; } }
Transform m_MergeTarget;
float m_mergeUntil;
static WL.UI.WLCombatTextSettings St
{
get { return WL.UI.WLCombatTextSettings.Enabled ? WL.UI.WLCombatTextSettings.Instance : null; }
}
/// <summary>합산 윈도우 길이 — 데이터 SOT 는 811e(WLReactionSettings). 없으면 합산하지 않는다.</summary>
static float MergeWindow
{
get
{
var r = WL.Combat.Reaction.WLReactionSettings.Instance;
if (r == null || !r.enabled || !r.damageMergeEnabled) return 0f;
return r.damageMergeWindowSeconds;
}
}
public void Set(Transform _target, string TextColor, double Damage, eStat Critical)
{
if (NewGameUI.Ins.m_BangchiUI.isActiveAndEnabled || !OptionInfo.Ins.go_check[0].activeSelf)
{
Off(true);
return;
}
SetCore(_target, TextColor, Damage, Critical);
}
/// <summary>
/// 표시 본체 — 원본 `Set` 의 UI 게이트(방치UI · 옵션) 뒤 부분을 그대로 뺀 것.
/// 게이트가 `NewGameUI`·`OptionInfo` 싱글턴을 요구해 Play 없이는 못 부르므로,
/// 에디트 모드 프로브(WL-813g ⓑ)가 **런타임과 같은 코드**를 재도록 분리했다(813c 교훈).
/// </summary>
public void SetCore(Transform _target, string TextColor, double Damage, eStat Critical)
{
var st = St;
bool wl = st != null && st.damageRhythmEnabled;
// ── 합산 윈도우: 같은 대상 · 같은 색이면 새 숫자를 띄우지 않고 살아 있는 숫자에 더한다(기준서 §B 0.25 s)
if (wl && st.damageUseMergeWindow && _target != null)
{
float win = MergeWindow;
if (win > 0f)
{
for (int i = 0; i < s_active.Count; i++)
{
var h = s_active[i];
if (h == null || h == this) continue;
if (h.m_MergeTarget != _target || h.m_color != TextColor) continue;
if (Time.time > h.m_mergeUntil) continue;
h.Accumulate(Damage, Critical, st);
MergedCount++;
Off(true); // 이 인스턴스는 쓰지 않고 풀로 돌려보낸다
return;
}
}
}
gameObject.SetActive(true);
Set_Target(_target);
if (m_Target != null && m_Camera != null && m_CanvasRect != null) Set_Pos(); // 대상·카메라·캔버스가 다 있을 때만(원본은 항상 있다)
usedmg2 = Critical == eStat.CRI || Critical == eStat.M_CRI;
m_Index = usedmg2 ? 1 : 0;
dmgs[0].enabled = !usedmg2;
dmgs[1].enabled = usedmg2;
dmgs[0].alpha = dmgs[1].alpha = 1f;
m_wl = wl;
m_crit = usedmg2;
m_color = TextColor;
m_total = Damage;
m_hits = 1;
m_MergeTarget = _target;
float scatter = wl ? st.damageScatterPx * (usedmg2 ? st.damageCritScatterMultiplier : 1f) : 25f;
Vector3 randomPosition = new Vector3(
Random.Range(-scatter, scatter), // x 축 랜덤 범위
Random.Range(-scatter, scatter)); // y 축 랜덤 범위
dmgs[0].transform.localPosition = dmgs[1].transform.localPosition = randomPosition;
if (wl)
{
m_lifeMax = m_life = usedmg2 ? st.damageCritSeconds : st.damageNormalSeconds;
m_scale = usedmg2 ? st.damageCritScale : st.damageNormalScale;
m_rise = st.damageRiseSpeed * (usedmg2 ? st.damageCritRiseMultiplier : 1f);
m_fadeStart = Mathf.Clamp01(st.damageFadeStartRatio);
m_mergeUntil = Time.time + Mathf.Max(0f, MergeWindow);
dmgs[m_Index].transform.localScale = Vector3.one * m_scale;
OffTime = m_lifeMax;
Register(st);
}
else
{
m_lifeMax = m_life = destroyTime;
m_scale = 1f; m_rise = moveSpeed; m_fadeStart = 0f;
m_mergeUntil = 0f;
dmgs[0].transform.localScale = dmgs[1].transform.localScale = Vector3.one;
OffTime = destroyTime;
Register(null);
}
ApplyEdge(st);
Redraw(TextColor, st);
isUpdate = false;
}
/// <summary>WL-813y — TMP 아웃라인/그림자(값은 WLCombatTextSettings.damageEdge · 인스턴스당 1회).</summary>
void ApplyEdge(WL.UI.WLCombatTextSettings st)
{
if (m_edgeDone || dmgs == null) return;
if (st == null) { m_edgeDone = true; return; } // 설정 off = 원본 머티리얼 그대로
var e = st.Edge(st.damageEdge);
for (int i = 0; i < dmgs.Length; i++)
if (dmgs[i] != null) LastEdgeDump = WL.UI.WLTextFxUtil.ApplyEdge(dmgs[i], e);
m_edgeDone = true;
EdgeAppliedCount++;
}
/// <summary>프로브 — 설정을 바꾼 뒤 살아 있는 인스턴스 전체에 다시 입힌다.</summary>
public static string RefreshEdges()
{
var all = Object.FindObjectsByType<HUDDMGUI>(FindObjectsInactive.Include, FindObjectsSortMode.None);
EdgeAppliedCount = 0;
for (int i = 0; i < all.Length; i++) { all[i].m_edgeDone = false; all[i].ApplyEdge(St); }
return "HUDDMGUI 인스턴스 " + all.Length + "개 중 적용 " + EdgeAppliedCount + " · " + LastEdgeDump;
}
/// <summary>합산 윈도우 안의 추가 타격 — 숫자를 더하고, 크리가 섞이면 크리 규격으로 승격한다.</summary>
void Accumulate(double Damage, eStat Critical, WL.UI.WLCombatTextSettings st)
{
m_total += Damage;
m_hits++;
bool crit = Critical == eStat.CRI || Critical == eStat.M_CRI;
if (crit && !m_crit)
{
m_crit = usedmg2 = true;
var pos = dmgs[m_Index].transform.localPosition;
m_Index = 1;
dmgs[0].enabled = false; dmgs[1].enabled = true;
dmgs[1].transform.localPosition = pos;
m_scale = st.damageCritScale;
m_lifeMax = st.damageCritSeconds;
m_rise = st.damageRiseSpeed * st.damageCritRiseMultiplier;
}
m_life = m_lifeMax; // 창이 이어지는 동안 표시 시간을 되감는다
OffTime = m_lifeMax;
dmgs[m_Index].alpha = 1f;
dmgs[m_Index].transform.localScale = Vector3.one * m_scale;
m_mergeUntil = Time.time + Mathf.Max(0f, MergeWindow);
Redraw(m_color, st);
}
void Redraw(string TextColor, WL.UI.WLCombatTextSettings st)
{
string num = NumberFormatter.FormatNumber(m_total);
if (m_wl && st != null && m_hits >= Mathf.Max(2, st.damageMergeHitsMin) && !string.IsNullOrEmpty(st.damageMergeHitsSuffix))
num = num + DSUtil.Format(st.damageMergeHitsSuffix, m_hits);
dmgs[0].text = dmgs[1].text = DSUtil.Format("{0}{1}", TextColor, num);
}
// ── 동시 표시 상한 (기준서 §B 요소 2 「초당 3~5개 겹침」 방지)
void Register(WL.UI.WLCombatTextSettings st)
{
if (!s_active.Contains(this)) s_active.Add(this);
if (st == null || st.damageMaxConcurrent <= 0) return;
while (s_active.Count > st.damageMaxConcurrent)
{
int victim = -1;
if (st.damageEvictNormalFirst)
for (int i = 0; i < s_active.Count; i++)
if (s_active[i] != null && s_active[i] != this && !s_active[i].m_crit) { victim = i; break; }
if (victim < 0)
for (int i = 0; i < s_active.Count; i++)
if (s_active[i] != null && s_active[i] != this) { victim = i; break; }
if (victim < 0) break;
var h = s_active[victim];
EvictedCount++;
h.Off(true); // OnDisable 이 목록에서 뺀다
if (s_active.Count > victim && s_active[victim] == h) s_active.RemoveAt(victim);
}
}
void OnDisable()
{
s_active.Remove(this);
m_MergeTarget = null;
m_mergeUntil = 0f;
if (dmgs != null)
{
if (dmgs.Length > 0 && dmgs[0] != null) dmgs[0].transform.localScale = Vector3.one;
if (dmgs.Length > 1 && dmgs[1] != null) dmgs[1].transform.localScale = Vector3.one;
}
}
/// <summary>프로브(에디트 모드)용 상태 덤프 — 실측 값만.</summary>
public string DumpState()
{
var t = dmgs[m_Index].transform;
return "wl=" + m_wl + " crit=" + m_crit + " hits=" + m_hits + " total=" + m_total.ToString("N0") +
" life=" + m_life.ToString("F2") + "/" + m_lifeMax.ToString("F2") +
" scale=" + t.localScale.x.ToString("F3") + " rise=" + m_rise.ToString("F2") +
" fadeStart=" + m_fadeStart.ToString("F2") + " alpha=" + dmgs[m_Index].alpha.ToString("F2") +
" idx=" + m_Index + " enabled0/1=" + dmgs[0].enabled + "/" + dmgs[1].enabled +
" localPos=" + t.localPosition + " text=\"" + dmgs[m_Index].text + "\"";
}
/// <summary>프로브용 — 지금 설정이 만들어 내는 리듬 값(코드 상수 0 확인용).</summary>
public static string SettingsDump()
{
var st = St;
if (st == null) return "WLCombatTextSettings 없음/off — 원본 상수(0.9 s · 배율 1 · 상한 없음)로 동작";
return "rhythm=" + st.damageRhythmEnabled + " normal=" + st.damageNormalSeconds + "s crit=" + st.damageCritSeconds +
"s critScale=" + st.damageCritScale + " normalScale=" + st.damageNormalScale +
" maxConcurrent=" + st.damageMaxConcurrent + " evictNormalFirst=" + st.damageEvictNormalFirst +
" mergeWindow=" + MergeWindow.ToString("F2") + "s(811e) useMerge=" + st.damageUseMergeWindow +
" scatterPx=" + st.damageScatterPx + " critScatterMul=" + st.damageCritScatterMultiplier;
}
/// <summary>프로브용 — 활성 목록을 비운다(에디트 모드 반복 실행 대비).</summary>
public static void ClearActiveList() { s_active.Clear(); EvictedCount = MergedCount = 0; }
private void LateUpdate()
{
var t = dmgs[m_Index].transform;
if (!m_wl)
{ // ── 원본 경로 그대로
t.Translate(new Vector3(0, moveSpeed * Time.deltaTime, 0)); // 텍스트 위치
dmgs[m_Index].alpha = Mathf.Lerp(dmgs[m_Index].alpha, 0, Time.deltaTime * alphaSpeed); // 텍스트 알파값
return;
}
m_life -= Time.deltaTime;
t.Translate(new Vector3(0, m_rise * Time.deltaTime, 0));
float passed = m_lifeMax > 0f ? 1f - Mathf.Clamp01(m_life / m_lifeMax) : 1f;
if (passed >= m_fadeStart)
{
float k = m_fadeStart >= 1f ? 1f : (passed - m_fadeStart) / (1f - m_fadeStart);
dmgs[m_Index].alpha = 1f - Mathf.Clamp01(k);
}
}
}