260 lines
12 KiB
C#
260 lines
12 KiB
C#
// WL-813g (#813) — 데미지 숫자 리듬화. 원본(힘민지) HUDDMGUI 에 표시 규칙만 얹는다.
|
|
// 기준서 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_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);
|
|
}
|
|
|
|
Redraw(TextColor, st);
|
|
isUpdate = false;
|
|
}
|
|
|
|
/// <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);
|
|
}
|
|
}
|
|
}
|