Project_WL/Assets/WL/Combat/Status/WLStatusText.cs

114 lines
6.5 KiB
C#

// ─────────────────────────────────────────────────────────────────────────────
// WLStatusText.cs — 상태 문구(「공격 속도 증가」 등) 월드 라벨 (#842)
// · 원본 데미지 HUD(NewGameUI 캔버스 · ScreenSpaceCamera)는 그 UI 카메라가 꺼져 있어 섬에서 보이지 않는다(2026-10-11 실측 · 데미지 숫자도 같은 상태).
// 그래서 섬 몹 라벨(「몬스터 n/10」 · WLIslandMobZones)과 같은 월드 TextMeshPro 로 머리 위에 띄운다 — 메인 카메라가 그린다.
// · 1.5 s 동안 떠오르며 뒤 절반에서 투명해진다. 같은 액터에 같은 문구가 살아 있으면 새로 띄우지 않고 시간만 되감는다(PD 「중복 재생 금지 · 시간 연장」).
// · 값(초·크기·상승 속도·페이드 시작 비율)은 WLStatus.json.
// ─────────────────────────────────────────────────────────────────────────────
using System.Collections.Generic;
using TMPro;
using UnityEngine;
namespace WL.Combat.Status
{
public static class WLStatusText
{
sealed class Item { public TextMeshPro tmp; public Actor actor; public string text; public Color color; public float startAt, lifeMax, yOff, rise, fadeStart; }
static readonly List<Item> s_items = new List<Item>(8);
static WLStatusTextRunner s_runner;
public static int Shown, Extended;
public static string LastLog = "";
public static void Show(Actor actor, string text, string colorTag, float seconds, float worldSize, float yOffset, float riseSpeed, float fadeStart)
{
if (actor == null || string.IsNullOrEmpty(text)) return;
float now = Time.time;
Color color = Color.white;
var m = System.Text.RegularExpressions.Regex.Match(colorTag ?? "", "#[0-9A-Fa-f]{6}");
if (m.Success) ColorUtility.TryParseHtmlString(m.Value, out color);
for (int i = 0; i < s_items.Count; i++)
{ // 같은 액터 · 같은 문구가 떠 있으면 연장
var e = s_items[i];
if (e.tmp == null || !e.tmp.gameObject.activeSelf || e.actor != actor || e.text != text) continue;
e.startAt = now; e.lifeMax = seconds; e.color = color; e.tmp.color = color;
Extended++; LastLog = "extend \"" + text + "\" → " + actor.name;
return;
}
var it = Get();
EnsureKoreanFont(it.tmp);
it.actor = actor; it.text = text; it.color = color; it.startAt = now; it.lifeMax = Mathf.Max(0.2f, seconds);
it.yOff = yOffset; it.rise = riseSpeed; it.fadeStart = Mathf.Clamp01(fadeStart);
it.tmp.text = text; it.tmp.fontSize = Mathf.Max(0.1f, worldSize); it.tmp.color = color;
Place(it, now);
it.tmp.gameObject.SetActive(true);
Shown++; LastLog = "show \"" + text + "\" → " + actor.name;
}
// TMP 기본 폰트(LiberationSans SDF)에는 한글이 없고 폴백도 0 이라 「공격 속도 증가」가 빨간 네모로 나온다(2026-10-11 실측).
// WLBossToken·WLEquipUi 와 같은 길로 인게임 한글 폰트(Font SDF)를 받아 붙인다 — 인게임에서 처음 띄울 때 받는다(타이틀 시점엔 아직 없다).
static TMP_FontAsset s_kr;
static void EnsureKoreanFont(TextMeshPro tmp)
{
if (tmp == null) return;
if (tmp.font != null && tmp.font.HasCharacter('가', true, true)) return;
if (s_kr == null || !s_kr.HasCharacter('가', true, true))
{
try { var kr = WL.Equip.WLEquipUi.ResolveKoreanFont(WL.Equip.WLEquipSettings.Instance); if (kr != null && kr.HasCharacter('가', true, true)) s_kr = kr; }
catch (System.Exception ex) { Debug.LogException(ex); }
}
if (s_kr != null) tmp.font = s_kr;
}
static Item Get()
{
for (int i = 0; i < s_items.Count; i++) if (s_items[i].tmp != null && !s_items[i].tmp.gameObject.activeSelf) return s_items[i];
if (s_runner == null) { var r = new GameObject("WL_StatusText"); Object.DontDestroyOnLoad(r); s_runner = r.AddComponent<WLStatusTextRunner>(); }
var go = new GameObject("WL_StatusTextItem");
go.transform.SetParent(s_runner.transform, false);
var tmp = go.AddComponent<TextMeshPro>(); // 섬 몹 라벨과 같은 기본 폰트(한글 포함)
tmp.alignment = TextAlignmentOptions.Center;
tmp.enableWordWrapping = false;
tmp.richText = false;
tmp.sortingOrder = 100;
go.SetActive(false);
var it = new Item { tmp = tmp };
s_items.Add(it);
return it;
}
static void Place(Item it, float now)
{
if (it.actor == null) return;
float t = now - it.startAt;
Vector3 p = it.actor.Get_Top_postion(); p.y += it.yOff + it.rise * t;
it.tmp.transform.position = p;
var cam = Camera.main;
if (cam != null) it.tmp.transform.rotation = cam.transform.rotation; // 빌보드
}
internal static void Tick()
{
float now = Time.time;
for (int i = 0; i < s_items.Count; i++)
{
var it = s_items[i];
if (it.tmp == null || !it.tmp.gameObject.activeSelf) continue;
float t = now - it.startAt;
if (it.actor == null || it.actor.IsDead() || !it.actor.gameObject.activeInHierarchy || t >= it.lifeMax) { it.tmp.gameObject.SetActive(false); continue; }
Place(it, now);
float k = it.lifeMax > 0f ? t / it.lifeMax : 1f;
float a = k <= it.fadeStart ? 1f : 1f - (k - it.fadeStart) / Mathf.Max(0.01f, 1f - it.fadeStart);
var c = it.color; c.a = Mathf.Clamp01(a); it.tmp.color = c;
}
}
/// <summary>프로브용 — 떠 있는 라벨 수.</summary>
public static int ActiveCount { get { int n = 0; for (int i = 0; i < s_items.Count; i++) if (s_items[i].tmp != null && s_items[i].tmp.gameObject.activeSelf) n++; return n; } }
}
public sealed class WLStatusTextRunner : MonoBehaviour
{
void LateUpdate() { WLStatusText.Tick(); }
}
}