// ───────────────────────────────────────────────────────────────────────────── // 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 s_items = new List(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(); } var go = new GameObject("WL_StatusTextItem"); go.transform.SetParent(s_runner.transform, false); var tmp = go.AddComponent(); // 섬 몹 라벨과 같은 기본 폰트(한글 포함) 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; } } /// 프로브용 — 떠 있는 라벨 수. 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(); } } }