// ───────────────────────────────────────────────────────────────────────────── // WLEquipGuiConfig.cs — 장비 GUI 설정 로더 + 스프라이트 접근 (#824 · 2026-10-09 → v3 2026-10-12 Layer Lab 재설계) // // 설정 = `Resources/WL/WLEquipGui.json`(WLEquipSettings.guiConfigPath · C45: 치수·색·스프라이트 이름·아이콘 대응 전부 데이터). // 스프라이트 검색 순서 = uiFolder(Layer Lab · `Assets/WL/UI/LayerLab/Resources/WLLayerLab` · 커밋 대상) // → refFolder(XP Hero 더미 · 로컬 전용 · 무기 아이콘만 남음) → placeholderFolder(임시 부위 아이콘). // 없으면 null 을 돌려주고 GUI 는 색 사각형으로 대체한다. // ───────────────────────────────────────────────────────────────────────────── using System.Collections.Generic; using Newtonsoft.Json; using UnityEngine; namespace WL.Equip { /// 상세 팝업·공통 치수(v2 에서 유지). [System.Serializable] public class WLGuiLayout { public float closeBtnW = 96, closeBtnH = 96, titleFontPx = 48; public float currencyW = 240, currencyH = 76, currencyIcon = 64, currencyFontPx = 32; public float popupW = 920, popupH = 1300, popupTitleH = 150, popupNameFontPx = 44, popupRarityFontPx = 30; public float headerH = 230, headerForm = 200, headerNameFontPx = 36, headerDpsH = 84, headerDpsFontPx = 30; public float optionFontPx = 28; public float popupBtnH = 104, popupBtnFontPx = 32; public float cellIconInset = 26, cellLevelH = 44, cellBadge = 54, cellFontPx = 26; public float gridCell = 180; } /// 장비 허브 화면(레퍼런스 1 · 캐릭터 중심 + 좌우 슬롯 + 탭/합성 + 5열 그리드) 치수. [System.Serializable] public class WLGuiHub { public float edge = 24, topBarH = 150; public float autoBtnW = 250, autoBtnH = 96, autoFontPx = 34; public float pillW = 240, pillH = 76, pillIcon = 64, pillFontPx = 32, pillGap = 12, pillAddBtn = 56; public float stageTop = 150, stageH = 830; public float previewW = 760, previewH = 800, previewY = -10; public float glowW = 820, glowH = 540, glowY = 40, platformW = 640, platformH = 200, platformY = 60; public float slotSize = 190, slotGap = 34, slotX = 30, slotTop = 50, slotIconInset = 34, slotFontPx = 28, slotLvH = 44, slotBadge = 60, slotNameFontPx = 26; public float statPillW = 330, statPillH = 84, statIcon = 60, statFontPx = 36, statGap = 24, statY = 760; public float tabTop = 1000, tabH = 104, tabGap = 8, tabIcon = 64, composeBtnW = 230, composeBtnH = 104, composeFontPx = 40; public float gridTop = 1130, gridBottom = 1800; public int gridCols = 5; public float gridCell = 180, gridGap = 12, gridPad = 20, cellIconInset = 26, cellPin = 52, cellFontPx = 26, cellTagH = 44, cellLvH = 40; public float cellRim = 6f, slotRim = 8f; // 등급 색 테두리 두께(등급 색 판 위에 어두운 속을 이만큼 작게 얹는다) // 2026-10-12 캐주얼 톤(SuperCasual) — 상단 띠 · 아래 남색 패널 · 탭 띠/밑줄 · 글자 외곽선 public float topBarFill = 0f; // > 0 이면 화면 위에서 이 높이까지 colors.topBar 띠 public float lowerPanelTop = 0f; // > 0 이면 이 y(px · 위에서)부터 바닥까지 colors.lowerPanel 패널 public float tabStripPad = 0f; // 탭 띠 배경이 탭 위아래로 더 나가는 길이 public float tabLineH = 0f; // > 0 이면 선택 탭 밑줄 높이(colors.tabLine) public float backFlushLeft = 0f; // 1 이면 뒤로 가기 버튼을 왼쪽 가장자리에 붙인다(레퍼런스 LeftFlush) public float backBtnW = 0f; // > 0 이면 뒤로 가기 버튼 너비(아니면 backBtn 정사각) public float shadowAtFeet = 0f; // 1 이면 무대 그림자(platform)를 미리보기 카메라 값으로 계산한 발밑에 둔다(platformY = 미세 보정 px) public float backBtn = 96, backY = 24, cheatBtnW = 220, cheatBtnH = 72, messageFontPx = 26; // 3D 미리보기 카메라(런타임 RenderTexture) public int previewRtW = 768, previewRtH = 800; public float camOrtho = 1.25f, camY = 0.95f, camDist = 6f, modelYaw = 180f, lightIntensity = 0f, lightPitch = 40f, lightYaw = -30f; public float rigY = -2000f; /// #879 슬롯 자리별 부위(eFREquipmentType 번호) — 순서 = 왼쪽 위 · 왼쪽 가운데 · 왼쪽 아래 · 오른쪽 위 · 오른쪽 가운데 · 오른쪽 아래. 비면 1~6 순서. public int[] slotTypes; // #881 PD 레퍼런스(인벤토리 화면) — 1 = 켬 · 0 = 이전 배치 public int headerMode = 0; // 부위 탭 줄 대신 「정렬 · 나의 장비 · 합성」 머리줄 public float headerH = 92, headerBtnW = 230, headerFontPx = 44, headerBtnFontPx = 34; public int gridHideEquipped = 0; // 그리드에서 장착 중인 장비를 뺀다(장착분은 슬롯에) public int slotPin = 0; // 슬롯에도 부위 핀(왼쪽 위) public int cellRefLayout = 0; // 칸 표시 = Lv 오른쪽 위 · S 왼쪽 아래 · +단계 마름모 아래 가운데 public float plusSize = 40, plusFontPx = 24; public string[] sortLabels = new string[0]; // 정렬 버튼 글자(0 = 등급순 · 1 = 레벨순) public string gridTitle = ""; } /// #881 장비 상세 팝업 V2(PD 레퍼런스) — 등급 리본 · 제목 띠 · 아이콘 + 레벨 · 능력치 + 다음 레벨 증가분 · 짧은 설명 · 등급 스킬 목록 · 비용 · 버튼 3. [System.Serializable] public class WLGuiItemPopup { public int enabled = 0; public float w = 900, h = 1220, ribbonW = 300, ribbonH = 56, ribbonFontPx = 28, titleH = 120, titleFontPx = 48, pad = 30; public float iconSize = 210, levelFontPx = 28, statH = 92, statIcon = 64, statFontPx = 44, statDeltaFontPx = 32, smallBtn = 92, smallBtnFontPx = 24, gap = 16; public float descH = 132, descFontPx = 26, sectionTitleH = 58, sectionTitleFontPx = 30, rowH = 50, rowFontPx = 26, dotSize = 26, lockSize = 34; public float costH = 60, costIcon = 46, costFontPx = 30, btnH = 108, btnFontPx = 36; public string sectionTitle = "등급 스킬", levelPrefix = "레벨: ", maxLevelText = "최대 레벨", noSkillText = "등급 스킬 없음"; public Dictionary statIcons = new Dictionary(); /// 무기 등급 스킬 줄(0 비움 · 1 일반 … 7 신화) — {main} = 주력 스킬 이름 · {awaken} = 각성 스킬 이름. public string[] weaponTierText = new string[0]; public int rowMaxParts = 3; } /// #879 장비 파츠 스킬 — 장비 화면 확장(슬롯 아래 능력치 · 스킬 뱃지 · 장착 스킬 줄 · 시너지 알림 · 스킬 상세 · 시너지 도감) 치수. [System.Serializable] public class WLGuiPartSkill { public int enabled = 1; public float slotLabelH = 56, slotLabelFontPx = 24, slotStatFontPx = 20; public float badge = 62, badgeInset = -10, badgeLvFontPx = 20; public float stripTop = 994, stripH = 96, stripPad = 24, chip = 88, chipGap = 14, passiveW = 196, passiveH = 64, chipFontPx = 22, bookBtnW = 150, bookBtnH = 72, bookBtnFontPx = 26; public float synTop = 1094, synH = 44, synFontPx = 22; public int passiveRound = 0, stripCenter = 0, bookRound = 0; // #881 레퍼런스 = 둥근 칩 한 줄 가운데 정렬(패시브 · 도감도 원) · synH 0 = 시너지 글줄 숨김 public float detailW = 940, detailH = 1440, detailTitleH = 150, detailFontPx = 26, detailRowH = 58, detailIcon = 150, detailBoxPad = 18; public float bookW = 940, bookH = 1440, bookRowH = 112, bookFontPx = 26; } /// 합성 화면(레퍼런스 2 · 말풍선 + 결과/재료 슬롯 + 장인 초상 + 필터/합성 + 그리드) 치수. [System.Serializable] public class WLGuiCompose { public float bubbleTop = 80, bubbleH = 150, bubbleW = 1000, bubbleFontPx = 42; public float stageTop = 260, stageH = 600; public float resultSize = 260, resultX = 110, resultY = 30, arrowSize = 72, arrowGap = 10; public float baseSize = 170, matSize = 140, matGap = 14, slotFontPx = 24; public float portraitW = 560, portraitH = 560, portraitX = -10, portraitY = 0; public string portrait = "character_sample_02"; public float infoFontPx = 28, infoW = 520, infoH = 120; public float btnRowY = 900, btnH = 104, filterBtnW = 230, allBtnW = 250, composeBtnW = 230, btnGap = 16, btnFontPx = 38; public float gridTop = 1030, gridBottom = 1800; } /// 뽑기 연출 치수·시간(#825 · 바닥 시트 + 결과 화면 + 확률 팝업). [System.Serializable] public class WLGuiGacha { public float sheetW = 1000, sheetH = 860, sheetBottom = 100, gradExtra = 300, titleH = 150, titleFontPx = 46, edge = 40; public float chestSize = 320, glowSize = 520, chestX = 260, chestY = 420; public float infoFontPx = 30, infoX = 470, infoY = 300, infoW = 490, infoH = 260, logH = 64; public float drawBtnW = 290, drawBtnH = 120, drawBtnBottom = 60, drawGap = 16, drawFontPx = 34; public float ratesBtn = 120, ratesPillW = 201, cheatBtnW = 240, cheatBtnH = 72; public float resultCard = 180, resultCardBig = 300, resultCols = 5, resultGap = 18, resultGlow = 900, resultGlowY = 60, resultGridY = 60, resultTitleFontPx = 64, resultTitleTop = 420, resultNameFontPx = 28; public float againBtnW = 432, againBtnH = 120, againY = 442, closeBtnW = 338, closeBtnH = 100, closeY = 202; public float shakeSec = 0.6f, shakeDeg = 9f, flashSec = 0.35f, revealStagger = 0.12f, popSec = 0.28f, starCount = 8, starSize = 64, starFly = 240, starSec = 0.6f; public float ratesW = 1000, ratesH = 1170, ratesRowH = 64, ratesFontPx = 30, barH = 36; } [System.Serializable] public class WLGuiTierSprites { public string bg; public string label; } [System.Serializable] public class WLGuiTab { public int id; public string label; public string icon; } [System.Serializable] public class WLGuiGroupTints { public List list; } [System.Serializable] public class WLEquipGuiConfig { public string uiFolder = "WLLayerLab"; public string uiFallbackFolder = ""; // uiFolder 에 없을 때 다음으로 찾는 폴더(캐주얼 키트에 없는 부위 아이콘 = 판타지 키트) public string artFolder = "WLEquipArt"; // WL 자체 그림(ComfyUI 생성 · 합성 장인 등) — Assets/WL/UI/Equip/Art/Resources/WLEquipArt public float textOutline = 0f; // > 0 이면 버튼·레벨 글자에 외곽선(공유 머티리얼 1장 · colors.textOutline) public string refFolder = "XPHeroRef"; public string placeholderFolder = "WLEquipPlaceholder"; public float scale = 1.5f; public int tintTierBg = 1; public WLGuiLayout layout = new WLGuiLayout(); public WLGuiHub hub = new WLGuiHub(); public WLGuiCompose compose = new WLGuiCompose(); public WLGuiGacha gacha = new WLGuiGacha(); public WLGuiPartSkill partSkill = new WLGuiPartSkill(); public WLGuiItemPopup itemPopup = new WLGuiItemPopup(); public Dictionary colors = new Dictionary(); public Dictionary sprites = new Dictionary(); public Dictionary tierSprites = new Dictionary(); public Dictionary> weaponIcons = new Dictionary>(); public Dictionary partIcons = new Dictionary(); public WLGuiGroupTints groupTints = new WLGuiGroupTints(); public Dictionary consumableIcons = new Dictionary(); public List tabs = new List(); public Color Col(string key, Color fallback) { float[] c; if (colors != null && colors.TryGetValue(key, out c) && c != null && c.Length >= 3) return new Color(c[0], c[1], c[2], c.Length >= 4 ? c[3] : 1f); return fallback; } public string Spr(string key) { string s; return sprites != null && sprites.TryGetValue(key, out s) ? s : null; } public WLGuiTierSprites Tier(int tier) { WLGuiTierSprites t; return tierSprites != null && tierSprites.TryGetValue(tier.ToString(), out t) ? t : null; } } public static class WLEquipGuiAssets { static WLEquipGuiConfig s_cfg; static string s_loadedPath; static readonly Dictionary s_sprites = new Dictionary(); public static string LastLog { get; private set; } = ""; public static int Missing { get; private set; } public static readonly List MissingNames = new List(); public static WLEquipGuiConfig Config { get { var es = WLEquipSettings.Instance; string path = es != null && !string.IsNullOrEmpty(es.guiConfigPath) ? es.guiConfigPath : "WL/WLEquipGui"; if (s_cfg != null && s_loadedPath == path) return s_cfg; try { var ta = Resources.Load(path); if (ta == null) throw new System.IO.FileNotFoundException("Resources/" + path + ".json 없음"); s_cfg = JsonConvert.DeserializeObject(ta.text); if (s_cfg.layout == null) s_cfg.layout = new WLGuiLayout(); if (s_cfg.hub == null) s_cfg.hub = new WLGuiHub(); if (s_cfg.compose == null) s_cfg.compose = new WLGuiCompose(); if (s_cfg.gacha == null) s_cfg.gacha = new WLGuiGacha(); if (s_cfg.partSkill == null) s_cfg.partSkill = new WLGuiPartSkill(); if (s_cfg.itemPopup == null) s_cfg.itemPopup = new WLGuiItemPopup(); s_loadedPath = path; LastLog = "GUI 설정 로드 — 스프라이트 키 " + (s_cfg.sprites != null ? s_cfg.sprites.Count : 0) + " · 탭 " + (s_cfg.tabs != null ? s_cfg.tabs.Count : 0); } catch (System.Exception ex) { LastLog = "GUI 설정 로드 실패 — " + ex.Message; Debug.LogError("[WL-824] " + LastLog); s_cfg = new WLEquipGuiConfig(); s_loadedPath = path; } return s_cfg; } } /// 절차 생성 원형 글로우(가운데 흰색 → 가장자리 투명). json 에서 이름 "@radial" 로 지정한다. public static Sprite RadialGlow() { Sprite s; if (s_sprites.TryGetValue("@radial", out s) && s != null) return s; const int N = 128; var tex = new Texture2D(N, N, TextureFormat.RGBA32, false); tex.wrapMode = TextureWrapMode.Clamp; tex.filterMode = FilterMode.Bilinear; var px = new Color32[N * N]; float c = (N - 1) * 0.5f; for (int y = 0; y < N; y++) for (int x = 0; x < N; x++) { float d = Mathf.Sqrt((x - c) * (x - c) + (y - c) * (y - c)) / c; float a = Mathf.Clamp01(1f - d); a = a * a * (3f - 2f * a); px[y * N + x] = new Color32(255, 255, 255, (byte)Mathf.RoundToInt(a * 255f)); } tex.SetPixels32(px); tex.Apply(false, true); s = Sprite.Create(tex, new Rect(0, 0, N, N), new Vector2(0.5f, 0.5f), 100f); s.name = "@radial"; s_sprites["@radial"] = s; return s; } /// 이름으로 스프라이트(uiFolder → refFolder → placeholderFolder 순). 없으면 null(호출자가 색 사각형으로 대체). public static Sprite Get(string name) { if (string.IsNullOrEmpty(name)) return null; if (name == "@radial") return RadialGlow(); Sprite s; if (s_sprites.TryGetValue(name, out s)) return s; var cfg = Config; s = !string.IsNullOrEmpty(cfg.artFolder) ? Resources.Load(cfg.artFolder + "/" + name) : null; if (s == null) s = Resources.Load(cfg.uiFolder + "/" + name); if (s == null && !string.IsNullOrEmpty(cfg.uiFallbackFolder)) s = Resources.Load(cfg.uiFallbackFolder + "/" + name); if (s == null) s = Resources.Load(cfg.refFolder + "/" + name); if (s == null) s = Resources.Load(cfg.placeholderFolder + "/" + name); if (s == null) { Missing++; if (MissingNames.Count < 50 && !MissingNames.Contains(name)) MissingNames.Add(name); if (WLEquipSettings.Instance != null && WLEquipSettings.Instance.verboseLog != 0 && Missing <= 20) Debug.LogWarning("[WL-824] 스프라이트 없음: " + name); } s_sprites[name] = s; return s; } public static Sprite GetByKey(string key) { return Get(Config.Spr(key)); } public static void ClearCache() { s_sprites.Clear(); s_cfg = null; Missing = 0; MissingNames.Clear(); } } }