Project_WL/Assets/WL/Equip/WLEquipGuiConfig.cs

277 lines
18 KiB
C#

// ─────────────────────────────────────────────────────────────────────────────
// 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
{
/// <summary>상세 팝업·공통 치수(v2 에서 유지).</summary>
[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;
}
/// <summary>장비 허브 화면(레퍼런스 1 · 캐릭터 중심 + 좌우 슬롯 + 탭/합성 + 5열 그리드) 치수.</summary>
[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;
/// <summary>#879 슬롯 자리별 부위(eFREquipmentType 번호) — 순서 = 왼쪽 위 · 왼쪽 가운데 · 왼쪽 아래 · 오른쪽 위 · 오른쪽 가운데 · 오른쪽 아래. 비면 1~6 순서.</summary>
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 = "";
}
/// <summary>#881 장비 상세 팝업 V2(PD 레퍼런스) — 등급 리본 · 제목 띠 · 아이콘 + 레벨 · 능력치 + 다음 레벨 증가분 · 짧은 설명 · 등급 스킬 목록 · 비용 · 버튼 3.</summary>
[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<string, string> statIcons = new Dictionary<string, string>();
/// <summary>무기 등급 스킬 줄(0 비움 · 1 일반 … 7 신화) — {main} = 주력 스킬 이름 · {awaken} = 각성 스킬 이름.</summary>
public string[] weaponTierText = new string[0];
public int rowMaxParts = 3;
}
/// <summary>#879 장비 파츠 스킬 — 장비 화면 확장(슬롯 아래 능력치 · 스킬 뱃지 · 장착 스킬 줄 · 시너지 알림 · 스킬 상세 · 시너지 도감) 치수.</summary>
[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;
}
/// <summary>합성 화면(레퍼런스 2 · 말풍선 + 결과/재료 슬롯 + 장인 초상 + 필터/합성 + 그리드) 치수.</summary>
[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;
}
/// <summary>뽑기 연출 치수·시간(#825 · 바닥 시트 + 결과 화면 + 확률 팝업).</summary>
[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<float[]> 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<string, float[]> colors = new Dictionary<string, float[]>();
public Dictionary<string, string> sprites = new Dictionary<string, string>();
public Dictionary<string, WLGuiTierSprites> tierSprites = new Dictionary<string, WLGuiTierSprites>();
public Dictionary<string, List<string>> weaponIcons = new Dictionary<string, List<string>>();
public Dictionary<string, string> partIcons = new Dictionary<string, string>();
public WLGuiGroupTints groupTints = new WLGuiGroupTints();
public Dictionary<string, string> consumableIcons = new Dictionary<string, string>();
public List<WLGuiTab> tabs = new List<WLGuiTab>();
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<string, Sprite> s_sprites = new Dictionary<string, Sprite>();
public static string LastLog { get; private set; } = "";
public static int Missing { get; private set; }
public static readonly List<string> MissingNames = new List<string>();
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<TextAsset>(path);
if (ta == null) throw new System.IO.FileNotFoundException("Resources/" + path + ".json 없음");
s_cfg = JsonConvert.DeserializeObject<WLEquipGuiConfig>(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;
}
}
/// <summary>절차 생성 원형 글로우(가운데 흰색 → 가장자리 투명). json 에서 이름 "@radial" 로 지정한다.</summary>
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;
}
/// <summary>이름으로 스프라이트(uiFolder → refFolder → placeholderFolder 순). 없으면 null(호출자가 색 사각형으로 대체).</summary>
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<Sprite>(cfg.artFolder + "/" + name) : null;
if (s == null) s = Resources.Load<Sprite>(cfg.uiFolder + "/" + name);
if (s == null && !string.IsNullOrEmpty(cfg.uiFallbackFolder)) s = Resources.Load<Sprite>(cfg.uiFallbackFolder + "/" + name);
if (s == null) s = Resources.Load<Sprite>(cfg.refFolder + "/" + name);
if (s == null) s = Resources.Load<Sprite>(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(); }
}
}