Project_WL/Assets/WL/UI/Scripts/WLTextFxUtil.cs

226 lines
12 KiB
C#

// ─────────────────────────────────────────────────────────────────────────────
// WLTextFxUtil.cs — 전투 텍스트 연출 3종이 공유하는 소소한 UI 조립 헬퍼 (WL-813g · #813)
//
// KillChainText · LootToast · StatPopup 이 자기 노드를 스스로 만든다(BuildIfNeeded 패턴 · 813c BossHpBar 선례).
// 프리팹에는 **빈 노드 1개 + 컴포넌트**만 들어가고 자식·좌표는 런타임에 만든다 →
// NewGameUI.prefab diff 를 "노드 추가"로만 유지한다(발주서 §2 ⓒ).
//
// 값은 하나도 들고 있지 않다(전부 WLCombatTextSettings). 여기 있는 숫자는 Unity 기본값(앵커 0/1)뿐이다.
// ─────────────────────────────────────────────────────────────────────────────
using System.Collections.Generic;
using TMPro;
using UnityEngine;
namespace WL.UI
{
internal static class WLTextFxUtil
{
internal const int UILayer = 5; // 813c 와 같은 UI 레이어 인덱스
/// <summary>이름으로 자식을 찾고 없으면 RectTransform 자식을 만든다.</summary>
internal static RectTransform NewChild(RectTransform parent, string name)
{
var t = parent.Find(name) as RectTransform;
if (t != null) return t;
var go = new GameObject(name, typeof(RectTransform));
go.layer = UILayer;
t = (RectTransform)go.transform;
t.SetParent(parent, false);
t.localScale = Vector3.one;
t.localRotation = Quaternion.identity;
return t;
}
/// <summary>부모를 가득 채우도록 늘린다.</summary>
internal static void Stretch(RectTransform rt)
{
rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one;
rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero;
rt.pivot = new Vector2(0.5f, 0.5f);
}
/// <summary>자식 TextMeshProUGUI 를 만들거나 찾는다(레이캐스트 off · 폰트는 있으면 주입).</summary>
internal static TextMeshProUGUI NewText(RectTransform parent, string name, TMP_FontAsset font, TextAlignmentOptions align)
{
var t = NewChild(parent, name);
var label = t.GetComponent<TextMeshProUGUI>();
if (label == null) label = t.gameObject.AddComponent<TextMeshProUGUI>();
label.raycastTarget = false;
label.alignment = align;
// 줄바꿈·오버플로 API 는 TMP 버전마다 이름이 갈려(enableWordWrapping ↔ textWrappingMode) 건드리지 않는다 — 기본값 유지.
if (font != null) label.font = font;
return label;
}
/// <summary>px → 캔버스 유닛 계수(802b 규약 · 캔버스 폭 / 기준 폭). 캔버스가 없으면 1.</summary>
internal static float UnitsPerPx(Component c, WLCombatTextSettings s)
{
if (s == null) return 1f;
var canvas = c != null ? c.GetComponentInParent<Canvas>() : null;
return s.UnitsPerPx(canvas != null ? canvas.rootCanvas : null);
}
/// <summary>0 → 1 로 지난 비율(life 는 남은 시간).</summary>
internal static float Passed(float life, float lifeMax)
{
return lifeMax > 0f ? 1f - Mathf.Clamp01(life / lifeMax) : 1f;
}
/// <summary>fadeStart 비율 이후에만 1 → 0 으로 떨어지는 알파.</summary>
internal static float FadeAlpha(float passed, float fadeStart)
{
fadeStart = Mathf.Clamp01(fadeStart);
if (passed < fadeStart) return 1f;
if (fadeStart >= 1f) return 0f;
return 1f - Mathf.Clamp01((passed - fadeStart) / (1f - fadeStart));
}
/// <summary>등장 펀치 스케일(punchSeconds 안에 punchScale → 1 로 수렴).</summary>
internal static float PunchScale(float elapsed, float punchScale, float punchSeconds)
{
if (punchSeconds <= 0f || punchScale <= 1f) return 1f;
float k = Mathf.Clamp01(elapsed / punchSeconds);
return Mathf.Lerp(punchScale, 1f, k * k);
}
// ── 아웃라인 · 그림자 (WL-813y · Q3 결함 D-6) ─────────────────────────
// uGUI `Outline`/`Shadow`(BaseMeshEffect)는 **TextMeshPro 에 적용되지 않는다** —
// TMP_Text 는 IMeshModifier 를 부르지 않고 자체 메시를 만든다(실측 근거는 완료보고).
// 그래서 TMP 의 정식 경로인 **머티리얼 아웃라인(_OutlineWidth/_OutlineColor)** +
// **언더레이 그림자(UNDERLAY_ON · _UnderlayOffsetX/Y · _UnderlaySoftness)** 를 쓴다. 포스트 0.
// 라벨마다 인스턴스 머티리얼을 만들면 배칭이 깨지므로, (원본 머티리얼 + 값) 조합마다
// **변형 머티리얼 1장**을 캐시해 `fontSharedMaterial` 로 공유한다.
private static readonly Dictionary<string, Material> s_edgeMats = new Dictionary<string, Material>();
private static readonly Dictionary<Material, Material> s_edgeOrigin = new Dictionary<Material, Material>();
/// <summary>지금까지 만든 아웃라인 변형 머티리얼 수(진단).</summary>
internal static int EdgeMaterialCount { get { return s_edgeMats.Count; } }
/// <summary>TMP 라벨에 아웃라인 + 언더레이 그림자를 입힌다. 적용 결과 문자열(실측)을 돌려준다.</summary>
internal static string ApplyEdge(TMP_Text label, WLTextEdge edge)
{
if (label == null) return "label=null";
Material src = label.fontSharedMaterial;
if (src == null) return "sharedMaterial=null";
Material origin;
if (s_edgeOrigin.TryGetValue(src, out origin)) src = origin; // 이미 우리 변형이면 원본으로 되돌아가서 다시 만든다
if (!edge.enabled || (edge.outlineWidth <= 0f && edge.shadowColor.a <= 0f))
{
if (!ReferenceEquals(label.fontSharedMaterial, src)) { label.fontSharedMaterial = src; label.UpdateMeshPadding(); }
return "edge off → 원본 머티리얼(" + src.name + ")";
}
string key = src.GetInstanceID() + "|" + edge.Key();
Material mat;
if (!s_edgeMats.TryGetValue(key, out mat) || mat == null)
{
mat = new Material(src);
mat.name = src.name + " (WL813y Edge)";
mat.hideFlags = HideFlags.DontSave;
Write(mat, edge);
s_edgeMats[key] = mat;
s_edgeOrigin[mat] = src;
}
if (!ReferenceEquals(label.fontSharedMaterial, mat))
{
label.fontSharedMaterial = mat;
label.UpdateMeshPadding(); // 아웃라인이 글리프 밖으로 나가므로 패딩을 다시 잡는다
}
return Describe(mat);
}
private static void Write(Material m, WLTextEdge e)
{
if (m.HasProperty(ShaderUtilities.ID_OutlineWidth)) m.SetFloat(ShaderUtilities.ID_OutlineWidth, Mathf.Clamp01(e.outlineWidth));
if (m.HasProperty(ShaderUtilities.ID_OutlineColor)) m.SetColor(ShaderUtilities.ID_OutlineColor, e.outlineColor);
if (m.HasProperty(ShaderUtilities.ID_FaceDilate)) m.SetFloat(ShaderUtilities.ID_FaceDilate, Mathf.Clamp(e.faceDilate, -1f, 1f));
bool shadow = e.shadowColor.a > 0f && (Mathf.Abs(e.shadowOffset.x) > 0f || Mathf.Abs(e.shadowOffset.y) > 0f || e.shadowSoftness > 0f);
if (m.HasProperty(ShaderUtilities.ID_UnderlayColor))
{
m.SetColor(ShaderUtilities.ID_UnderlayColor, shadow ? e.shadowColor : new Color(0f, 0f, 0f, 0f));
if (m.HasProperty(ShaderUtilities.ID_UnderlayOffsetX)) m.SetFloat(ShaderUtilities.ID_UnderlayOffsetX, e.shadowOffset.x);
if (m.HasProperty(ShaderUtilities.ID_UnderlayOffsetY)) m.SetFloat(ShaderUtilities.ID_UnderlayOffsetY, e.shadowOffset.y);
if (m.HasProperty(ShaderUtilities.ID_UnderlaySoftness)) m.SetFloat(ShaderUtilities.ID_UnderlaySoftness, Mathf.Clamp01(e.shadowSoftness));
if (m.HasProperty(ShaderUtilities.ID_UnderlayDilate)) m.SetFloat(ShaderUtilities.ID_UnderlayDilate, 0f);
if (shadow) m.EnableKeyword("UNDERLAY_ON"); else m.DisableKeyword("UNDERLAY_ON");
}
if (m.HasProperty(ShaderUtilities.ID_OutlineWidth) && e.outlineWidth > 0f) m.EnableKeyword("OUTLINE_ON");
}
/// <summary>머티리얼에 실제로 들어간 값(실측 덤프용).</summary>
internal static string Describe(Material m)
{
if (m == null) return "(머티리얼 없음)";
var sb = new System.Text.StringBuilder();
sb.Append(m.name).Append(" shader=").Append(m.shader != null ? m.shader.name : "-");
sb.Append(" outlineW=").Append(m.HasProperty(ShaderUtilities.ID_OutlineWidth) ? m.GetFloat(ShaderUtilities.ID_OutlineWidth).ToString("F3") : "없음");
sb.Append(" outlineC=").Append(m.HasProperty(ShaderUtilities.ID_OutlineColor) ? ColorUtility.ToHtmlStringRGBA(m.GetColor(ShaderUtilities.ID_OutlineColor)) : "없음");
if (m.HasProperty(ShaderUtilities.ID_UnderlayColor))
{
sb.Append(" underlay=").Append(ColorUtility.ToHtmlStringRGBA(m.GetColor(ShaderUtilities.ID_UnderlayColor)));
sb.Append(" off(").Append(m.GetFloat(ShaderUtilities.ID_UnderlayOffsetX).ToString("F2")).Append(',')
.Append(m.GetFloat(ShaderUtilities.ID_UnderlayOffsetY).ToString("F2")).Append(')');
sb.Append(" soft=").Append(m.GetFloat(ShaderUtilities.ID_UnderlaySoftness).ToString("F2"));
sb.Append(" UNDERLAY_ON=").Append(m.IsKeywordEnabled("UNDERLAY_ON"));
}
return sb.ToString();
}
/// <summary>라벨을 원본 머티리얼로 되돌린다(프로브·롤백).</summary>
internal static string ClearEdge(TMP_Text label)
{
if (label == null) return "label=null";
Material origin;
if (label.fontSharedMaterial != null && s_edgeOrigin.TryGetValue(label.fontSharedMaterial, out origin))
{
label.fontSharedMaterial = origin;
label.UpdateMeshPadding();
return "원복 → " + origin.name;
}
return "변형 아님 — 그대로";
}
}
/// <summary>
/// 텍스트 테두리·그림자 값 묶음 (WL-813y · C8/C45 — 코드 상수 0 · 전부 SO).
/// TMP 머티리얼 규격 그대로다: outlineWidth 0~1 · shadowOffset 은 폰트 단위(±1) · softness 0~1.
/// </summary>
[System.Serializable]
public struct WLTextEdge
{
[Tooltip("이 텍스트에 아웃라인/그림자를 입힐지")]
public bool enabled;
[Tooltip("아웃라인 두께 (TMP _OutlineWidth · 0~1 · 0.2~0.3 이 육안으로 또렷)")]
[Range(0f, 1f)] public float outlineWidth;
[Tooltip("아웃라인 색")]
public Color outlineColor;
[Tooltip("그림자(언더레이) 오프셋 — 폰트 단위(±1). 예 (0.6, -0.6) = 우하단")]
public Vector2 shadowOffset;
[Tooltip("그림자 색 — 알파 0 이면 그림자 없음")]
public Color shadowColor;
[Tooltip("그림자 번짐 (TMP _UnderlaySoftness · 0~1)")]
[Range(0f, 1f)] public float shadowSoftness;
[Tooltip("글자 두께 보정 (TMP _FaceDilate · -1~1). 아웃라인이 글자를 먹으면 살짝 올린다")]
[Range(-1f, 1f)] public float faceDilate;
internal string Key()
{
return (enabled ? "1" : "0") + outlineWidth.ToString("F3") + outlineColor + shadowOffset + shadowColor +
shadowSoftness.ToString("F3") + faceDilate.ToString("F3");
}
public override string ToString()
{
return "on=" + enabled + " w=" + outlineWidth.ToString("F2") +
" oc=#" + ColorUtility.ToHtmlStringRGBA(outlineColor) +
" sh" + shadowOffset + " sc=#" + ColorUtility.ToHtmlStringRGBA(shadowColor) +
" soft=" + shadowSoftness.ToString("F2") + " dilate=" + faceDilate.ToString("F2");
}
}
}