ThreeKingdomsSlingshot/Assets/Scripts/Runtime/UI/UISkin.cs

151 lines
6.5 KiB
C#
Raw Permalink Normal View History

UI 리뉴얼 A-2~A-4 화면 적용(PD #1100) — 팩 부품 + UI 테마(id 치환) · 화면 10종 · 인박스 ui kind - 테마: SOT Assets/Data/UI/ui_theme.csv(색 57 · 스킨 32 · 아이콘 63 · 제목 폰트) → 메뉴 TKS/Import UI Theme → Resources/UI/UITheme.asset (Runtime/UI/UITheme · UISkin · Editor/UIThemeImporter · ProjectSetup 에서도 임포트) · 화면 코드는 id 로만 묻는다(스프라이트 경로 0) - 신규 제작(B) 치환: Assets/UI/Samguk/**/<id>.png → 임포트 시 같은 id 스킨·아이콘 자동 치환(팩 보존 · 새 id = 아이콘 추가) · Tools/apply_delivery.py ui kind(md5 → Samguk/<group> · gim_* → Resources/Gimmicks/Art · ui_delivered.csv) - 공통: UiKit(카드 = BorderFrame_Round20 틴트 · 버튼 = Button01_s + 광택 · 원 · 진행 바 = Slider_Basic01 · 초상 틀 · 등급 칸 ItemFrame02 · 별 줄 · 병종 배지) · 재화 바(ResourceBar + cur_* 아이콘) · 하단 탭(선택 판 + PictoIcon) · 제목 띠 · 팝업(Popup Topber + 닫기 X) · 토스트 - 화면: 로비(키아트 04 배경 위쪽 ×1.22 · 출정·편성·연무장 아이콘) · 영웅(등급 칸 + 장비 아이콘 · 체크/알림 점 · 별 아이콘) · 편성 · 거래(배너 · 상자 아이콘) · 보물(상자 아이콘 · 포인트 배지) · 내정(보상 줄 · 도면·골드 아이콘) · 연무장 · 결과(리본 · 별) · 월드맵 스테이지 팝업만 팩(거점 kit 유지) - 전투: HUD 칩 · 턴 띠 · 원형 스킬 버튼 + 쿨 라디얼 · 유닛 체력 바 · 기믹 내구 핍 = 팩 슬라이더(월드 9-slice 사본 · 가운데 2 px) · 조준선 색 유지 - 3:4(1536×2048): 본문 비율 축소(UiFitHeight · 1080×1920 = 그대로) - 검증: 플레이모드 116/116(기존 115 + UIThemeTests 1) · 컴파일 0 · error 0 · 캡처 = DataImporter.CaptureUI(UICaptureDriver · 15장) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 14:20:57 +00:00
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
namespace TKS
{
/// <summary>
/// UI 테마 적용 도우미(PD #1100) — 아웃게임 UiKit · 전투 HUD · 월드맵 팝업이 같이 쓴다.
/// 테마가 없으면 모든 함수가 false/null 을 돌려주고 호출부는 기존 도형으로 그린다.
/// </summary>
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;
}
/// <summary>이미지에 스킨(9-slice 바탕 + 덮개) 적용 · 색은 호출부(img.color)가 정한다.</summary>
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;
}
/// <summary>부모를 꽉 채우는 덮개(광택 등 · 첫 자식 = 내용 아래) · inset = 왼 · 위 · 오 · 아래.</summary>
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<Image>();
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;
}
/// <summary>아이콘 이미지(테마에 id 가 없으면 null) · 비율 유지 · 표 tint 적용.</summary>
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<Image>();
img.sprite = e.Sprite;
img.color = e.samguk != null ? Color.white : e.tint;
img.preserveAspect = true;
img.raycastTarget = false;
return img;
}
/// <summary>글자 외곽선(팩 버튼 글자처럼 굵은 어두운 선).</summary>
public static void Stroke(Graphic g, Color c, float d = 2.5f)
{
if (g == null || g.GetComponent<Outline>() != 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<Outline>();
o.effectColor = c;
o.effectDistance = new Vector2(d, -d);
}
static readonly Dictionary<string, Sprite> worldCache = new Dictionary<string, Sprite>();
/// <summary>
/// 월드 공간(SpriteRenderer · 9-slice)용 스킨 사본 — 같은 텍스처를 ppu 만 바꿔 다시 만든다(체력 바처럼 월드 크기가 작은 막대가 테두리보다 작아 깨지지 않게).
/// </summary>
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);
UI 리뉴얼 A-2~A-4 화면 적용(PD #1100) — 팩 부품 + UI 테마(id 치환) · 화면 10종 · 인박스 ui kind - 테마: SOT Assets/Data/UI/ui_theme.csv(색 57 · 스킨 32 · 아이콘 63 · 제목 폰트) → 메뉴 TKS/Import UI Theme → Resources/UI/UITheme.asset (Runtime/UI/UITheme · UISkin · Editor/UIThemeImporter · ProjectSetup 에서도 임포트) · 화면 코드는 id 로만 묻는다(스프라이트 경로 0) - 신규 제작(B) 치환: Assets/UI/Samguk/**/<id>.png → 임포트 시 같은 id 스킨·아이콘 자동 치환(팩 보존 · 새 id = 아이콘 추가) · Tools/apply_delivery.py ui kind(md5 → Samguk/<group> · gim_* → Resources/Gimmicks/Art · ui_delivered.csv) - 공통: UiKit(카드 = BorderFrame_Round20 틴트 · 버튼 = Button01_s + 광택 · 원 · 진행 바 = Slider_Basic01 · 초상 틀 · 등급 칸 ItemFrame02 · 별 줄 · 병종 배지) · 재화 바(ResourceBar + cur_* 아이콘) · 하단 탭(선택 판 + PictoIcon) · 제목 띠 · 팝업(Popup Topber + 닫기 X) · 토스트 - 화면: 로비(키아트 04 배경 위쪽 ×1.22 · 출정·편성·연무장 아이콘) · 영웅(등급 칸 + 장비 아이콘 · 체크/알림 점 · 별 아이콘) · 편성 · 거래(배너 · 상자 아이콘) · 보물(상자 아이콘 · 포인트 배지) · 내정(보상 줄 · 도면·골드 아이콘) · 연무장 · 결과(리본 · 별) · 월드맵 스테이지 팝업만 팩(거점 kit 유지) - 전투: HUD 칩 · 턴 띠 · 원형 스킬 버튼 + 쿨 라디얼 · 유닛 체력 바 · 기믹 내구 핍 = 팩 슬라이더(월드 9-slice 사본 · 가운데 2 px) · 조준선 색 유지 - 3:4(1536×2048): 본문 비율 축소(UiFitHeight · 1080×1920 = 그대로) - 검증: 플레이모드 116/116(기존 115 + UIThemeTests 1) · 컴파일 0 · error 0 · 캡처 = DataImporter.CaptureUI(UICaptureDriver · 15장) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 14:20:57 +00:00
sp.name = key;
sp.hideFlags = HideFlags.DontSave;
worldCache[key] = sp;
return sp;
}
}
}