ThreeKingdomsSlingshot/Assets/Scripts/Editor/UIThemeImporter.cs

237 lines
10 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 System.IO;
using System.Text;
using UnityEditor;
using UnityEngine;
namespace TKS.EditorTools
{
/// <summary>
/// UI 테마 임포트(PD #1100) — Assets/Data/UI/ui_theme.csv(SOT) → Assets/Resources/UI/UITheme.asset.
/// 경로 = 팩 Components 기준 상대 경로(Assets/ 로 시작하면 그대로) · 오류가 있으면 자산을 바꾸지 않는다.
/// 신규 제작 납품(Assets/UI/Samguk/**/&lt;id&gt;.png)은 같은 id 의 스킨·아이콘을 치환하고 표에 없는 id 는 아이콘으로 추가한다(코드 변경 없이 교체).
/// </summary>
public static class UIThemeImporter
{
public const string CsvPath = "Assets/Data/UI/ui_theme.csv";
public const string AssetPath = "Assets/Resources/UI/UITheme.asset";
public const string PackRoot = "Assets/UI/GUIPro/ResourcesData/Sprites/Components/";
public const string SamgukRoot = "Assets/UI/Samguk";
[MenuItem("TKS/Import UI Theme (UI 테마 임포트)")]
public static void Menu()
{
Import();
}
public static bool Import()
{
var errors = new List<string>();
var theme = ScriptableObject.CreateInstance<UITheme>();
if (!File.Exists(CsvPath))
{
Debug.LogError("[TKS] UI 테마 표 없음: " + CsvPath);
return false;
}
var lines = File.ReadAllText(CsvPath, Encoding.UTF8).TrimStart('').Replace("\r", "").Split('\n');
for (int n = 1; n < lines.Length; n++)
{
var line = lines[n];
if (string.IsNullOrWhiteSpace(line)) continue;
var f = line.Split(',');
if (f.Length < 8)
{
errors.Add((n + 1) + "행: 열 부족");
continue;
}
string kind = f[0].Trim(), id = f[1].Trim(), path = f[2].Trim(), overlay = f[3].Trim(), color = f[7].Trim();
switch (kind)
{
case "color":
{
Color c;
if (!ColorUtility.TryParseHtmlString(color, out c)) errors.Add((n + 1) + "행 " + id + ": 색 형식 " + color);
else theme.colors.Add(new UITheme.ColorEntry { id = id, color = c });
break;
}
case "skin":
{
var s = new UITheme.SkinEntry { id = id, pack = LoadSprite(path, n, errors) };
if (overlay.Length > 0) s.overlay = LoadSprite(overlay, n, errors);
s.ppu = ParseF(f[4], 1f);
s.inset = ParseInset(f[5]);
s.overlayAlpha = ParseF(f[6], 1f);
theme.skins.Add(s);
break;
}
case "icon":
{
var e = new UITheme.IconEntry { id = id, pack = LoadSprite(path, n, errors) };
Color c;
if (color.Length > 0 && ColorUtility.TryParseHtmlString(color, out c)) e.tint = c;
theme.icons.Add(e);
break;
}
case "font":
if (id == "title")
{
theme.titleFont = AssetDatabase.LoadAssetAtPath<Font>(path);
if (theme.titleFont == null) errors.Add((n + 1) + "행: 폰트 없음 " + path);
}
break;
default:
errors.Add((n + 1) + "행: 알 수 없는 kind " + kind);
break;
}
}
if (errors.Count > 0)
{
foreach (var e in errors) Debug.LogError("[TKS] UI 테마 표 오류 · " + e);
Debug.LogError("[TKS] UI 테마 임포트 중단(오류 " + errors.Count + ") · 자산 변경 없음");
Object.DestroyImmediate(theme);
return false;
}
// 신규 제작 치환(파일 이름 = id)
var samguk = SamgukSprites();
int replaced = 0, added = 0;
var used = new HashSet<string>();
foreach (var s in theme.skins)
{
Sprite sp;
if (samguk.TryGetValue(s.id, out sp))
{
s.samguk = sp;
used.Add(s.id);
replaced++;
}
}
foreach (var e in theme.icons)
{
Sprite sp;
if (samguk.TryGetValue(e.id, out sp))
{
e.samguk = sp;
used.Add(e.id);
replaced++;
}
}
foreach (var kv in samguk)
{
if (used.Contains(kv.Key)) continue;
theme.icons.Add(new UITheme.IconEntry { id = kv.Key, samguk = kv.Value });
added++;
}
if (!AssetDatabase.IsValidFolder("Assets/Resources")) AssetDatabase.CreateFolder("Assets", "Resources");
if (!AssetDatabase.IsValidFolder("Assets/Resources/UI")) AssetDatabase.CreateFolder("Assets/Resources", "UI");
var asset = AssetDatabase.LoadAssetAtPath<UITheme>(AssetPath);
if (asset == null)
{
AssetDatabase.CreateAsset(theme, AssetPath);
asset = theme;
}
else
{
EditorUtility.CopySerialized(theme, asset);
Object.DestroyImmediate(theme);
}
asset.Rebuild();
EditorUtility.SetDirty(asset);
AssetDatabase.SaveAssets();
UITheme.Use(asset);
Debug.Log("[TKS] UI 테마 임포트 · 색 " + asset.colors.Count + " · 스킨 " + asset.skins.Count + " · 아이콘 " + asset.icons.Count +
" · 신규 제작 치환 " + replaced + " · 추가 " + added + " → " + AssetPath);
// 테마가 쓰는 스프라이트 = UI 아틀라스 구성(PD 2026-11-10 · 새 스프라이트도 자동으로 묶임 · 구성이 같으면 그대로)
UIAtlasBuilder.Build(asset);
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
return true;
}
/// <summary>Assets/UI/Samguk/**/&lt;id&gt;.png → 스프라이트(같은 id 가 둘이면 경로 순 마지막).</summary>
public static Dictionary<string, Sprite> SamgukSprites()
{
var map = new Dictionary<string, Sprite>();
if (!AssetDatabase.IsValidFolder(SamgukRoot)) return map;
var guids = AssetDatabase.FindAssets("t:Sprite", new[] { SamgukRoot });
var paths = new List<string>();
foreach (var g in guids) paths.Add(AssetDatabase.GUIDToAssetPath(g));
paths.Sort(System.StringComparer.Ordinal);
foreach (var p in paths)
{
var sp = AssetDatabase.LoadAssetAtPath<Sprite>(p);
if (sp != null) map[Path.GetFileNameWithoutExtension(p)] = sp;
}
return map;
}
static Sprite LoadSprite(string path, int n, List<string> errors)
{
if (string.IsNullOrEmpty(path))
{
errors.Add((n + 1) + "행: 경로 없음");
return null;
}
string full = path.StartsWith("Assets/") ? path : PackRoot + path;
var sp = AssetDatabase.LoadAssetAtPath<Sprite>(full);
if (sp == null) errors.Add((n + 1) + "행: 스프라이트 없음 " + full);
return sp;
}
static float ParseF(string s, float def)
{
float v;
return float.TryParse(s.Trim(), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out v) ? v : def;
}
static Vector4 ParseInset(string s)
{
s = s.Trim();
if (s.Length == 0) return Vector4.zero;
var p = s.Split(';');
if (p.Length == 1)
{
float v = ParseF(p[0], 0f);
return new Vector4(v, v, v, v);
}
return new Vector4(ParseF(p[0], 0f), p.Length > 1 ? ParseF(p[1], 0f) : 0f, p.Length > 2 ? ParseF(p[2], 0f) : 0f, p.Length > 3 ? ParseF(p[3], 0f) : 0f);
}
}
/// <summary>신규 제작 UI(Assets/UI/Samguk) · 로비 배경(Assets/UI/Lobby) 임포트 설정 = Sprite(2D) · 납품 파일이 들어오면 테마 치환 테이블 자동 갱신.</summary>
public class UIThemeAssetPostprocessor : AssetPostprocessor
{
static bool pending;
static bool Ours(string path)
{
return path.StartsWith(UIThemeImporter.SamgukRoot + "/") || path.StartsWith("Assets/UI/Lobby/") || path.StartsWith("Assets/UI/Symbols/"); // Symbols = 병과 단색 심볼(Tools/class_symbols.py · PD 2026-11-09)
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
}
void OnPreprocessTexture()
{
if (!Ours(assetPath)) return;
var ti = (TextureImporter)assetImporter;
ti.textureType = TextureImporterType.Sprite;
ti.spriteImportMode = SpriteImportMode.Single;
ti.alphaIsTransparency = true;
ti.mipmapEnabled = false;
ti.spritePixelsPerUnit = 100f;
ti.maxTextureSize = assetPath.StartsWith("Assets/UI/Lobby/") ? 2048 : 1024;
}
static void OnPostprocessAllAssets(string[] imported, string[] deleted, string[] moved, string[] movedFrom)
{
bool hit = false;
foreach (var p in imported) hit |= p.StartsWith(UIThemeImporter.SamgukRoot + "/") && p.EndsWith(".png");
foreach (var p in deleted) hit |= p.StartsWith(UIThemeImporter.SamgukRoot + "/") && p.EndsWith(".png");
if (!hit || pending) return;
pending = true;
EditorApplication.delayCall += () =>
{
pending = false;
UIThemeImporter.Import();
};
}
}
}