using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; namespace TKS { /// /// UI 테마 적용 도우미(PD #1100) — 아웃게임 UiKit · 전투 HUD · 월드맵 팝업이 같이 쓴다. /// 테마가 없으면 모든 함수가 false/null 을 돌려주고 호출부는 기존 도형으로 그린다. /// public static class UISkin { public const string OverlayName = "Gloss"; public static UITheme Theme { get { return UITheme.Current; } } public static bool On { get { return Theme != null; } } public static Color Col(string id, Color fallback) { Color c; return Theme != null && Theme.TryColor(id, out c) ? c : fallback; } public static Font TitleFont { get { return Theme != null && Theme.titleFont != null ? Theme.titleFont : TextUtil.Font; } } public static bool Has(string skinId) { return Theme != null && Theme.Skin(skinId) != null; } /// 이미지에 스킨(9-slice 바탕 + 덮개) 적용 · 색은 호출부(img.color)가 정한다. public static bool Apply(Image img, string skinId) { var s = Theme != null ? Theme.Skin(skinId) : null; if (s == null || img == null) return false; var sp = s.Sprite; img.sprite = sp; img.type = sp.border != Vector4.zero ? Image.Type.Sliced : Image.Type.Simple; img.pixelsPerUnitMultiplier = s.ppu > 0f ? s.ppu : 1f; img.fillCenter = true; var old = img.transform.Find(OverlayName); if (old != null) Object.Destroy(old.gameObject); if (s.overlay != null) Overlay(img.transform, s.overlay, s.inset, s.ppu).color = new Color(1f, 1f, 1f, s.overlayAlpha); return true; } /// 부모를 꽉 채우는 덮개(광택 등 · 첫 자식 = 내용 아래) · inset = 왼 · 위 · 오 · 아래. public static Image Overlay(Transform parent, Sprite sp, Vector4 inset, float ppu = 1f) { var go = new GameObject(OverlayName, typeof(RectTransform)); go.transform.SetParent(parent, false); go.transform.SetAsFirstSibling(); var rt = (RectTransform)go.transform; rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one; rt.offsetMin = new Vector2(inset.x, inset.w); rt.offsetMax = new Vector2(-inset.z, -inset.y); var img = go.AddComponent(); img.sprite = sp; img.type = sp.border != Vector4.zero ? Image.Type.Sliced : Image.Type.Simple; img.pixelsPerUnitMultiplier = ppu > 0f ? ppu : 1f; img.preserveAspect = sp.border == Vector4.zero; img.raycastTarget = false; return img; } public static Sprite Icon(string id) { return Theme != null ? Theme.Icon(id) : null; } /// 아이콘 이미지(테마에 id 가 없으면 null) · 비율 유지 · 표 tint 적용. public static Image IconImage(Transform parent, string id, Vector2 anchor, Vector2 pivot, Vector2 pos, Vector2 size) { var e = Theme != null ? Theme.IconEntryOf(id) : null; if (e == null) return null; var go = new GameObject("Icon_" + id, typeof(RectTransform)); go.transform.SetParent(parent, false); var rt = (RectTransform)go.transform; rt.anchorMin = rt.anchorMax = anchor; rt.pivot = pivot; rt.anchoredPosition = pos; rt.sizeDelta = size; var img = go.AddComponent(); img.sprite = e.Sprite; img.color = e.samguk != null ? Color.white : e.tint; img.preserveAspect = true; img.raycastTarget = false; return img; } /// 글자 외곽선(팩 버튼 글자처럼 굵은 어두운 선). public static void Stroke(Graphic g, Color c, float d = 2.5f) { if (g == null || g.GetComponent() != null) return; var t = g as Text; if (t != null && t.fontSize < 40) d = Mathf.Min(d, t.fontSize < 30 ? 1.5f : 2f); // 작은 글자는 얇게(굵으면 한글이 뭉개짐) var o = g.gameObject.AddComponent(); o.effectColor = c; o.effectDistance = new Vector2(d, -d); } static readonly Dictionary worldCache = new Dictionary(); /// /// 월드 공간(SpriteRenderer · 9-slice)용 스킨 사본 — 같은 텍스처를 ppu 만 바꿔 다시 만든다(체력 바처럼 월드 크기가 작은 막대가 테두리보다 작아 깨지지 않게). /// public static Sprite WorldSprite(string skinId, float ppu) { var s = Theme != null ? Theme.Skin(skinId) : null; if (s == null) return null; string key = skinId + "@" + ppu; Sprite sp; if (worldCache.TryGetValue(key, out sp) && sp != null) return sp; var src = s.Sprite; // SpriteRenderer 9-slice 는 가운데 칸이 0 px 이면 가운데를 그리지 않는다(팩 슬라이더 = 테두리가 전부) → 가운데 2 px 를 남긴다 var b = src.border; float w = src.rect.width, h = src.rect.height; if (b.x + b.z > w - 2f) { float k = Mathf.Max(0f, w - 2f) / Mathf.Max(1f, b.x + b.z); b.x = Mathf.Floor(b.x * k); b.z = Mathf.Floor(b.z * k); } if (b.y + b.w > h - 2f) { float k = Mathf.Max(0f, h - 2f) / Mathf.Max(1f, b.y + b.w); b.y = Mathf.Floor(b.y * k); b.w = Mathf.Floor(b.w * k); } // 아틀라스에 묶인 스프라이트(PD 2026-11-10 UI 아틀라스) = texture 가 아틀라스 장 → 그 장 안의 자리(textureRect)로 잘라야 한다(rect = 원본 기준) var area = src.packed ? src.textureRect : src.rect; sp = Sprite.Create(src.texture, area, new Vector2(0.5f, 0.5f), ppu, 0, SpriteMeshType.FullRect, b); sp.name = key; sp.hideFlags = HideFlags.DontSave; worldCache[key] = sp; return sp; } } }