// ───────────────────────────────────────────────────────────────────────────── // WLEquipGrades.cs — WL 아이템 등급 표 로더 (PD 2026-10-09 · #823 7단계 → 2026-10-23 #973 8단계 G1~G8 · 천상 = FR Legend-3) // // 표 = `Resources/WL/WLGradeTable.json`(WLEquipSettings.gradeTablePath · C45 — 코드는 값을 갖지 않는다) // tiers : 등급 7단계(일반·고급·희귀·에픽·전설(S)·불멸·신화) 이름 · 색 · 주스탯 반영 % // frMap : FR 이식 데이터의 (ItemGrade, GradeLv) 14단계 → WL 등급(+단계) 대응 // parts : 부위별 출력 스탯(공격 부위 → ATK · 체력 부위 → MaxHP) · 레벨표 필드 · 주스탯 1당 환산 계수 // FR 표(ItemTable · 레벨표 · 합성표)는 수정하지 않는다 — 등급은 이 대응표로 읽는다. // ───────────────────────────────────────────────────────────────────────────── using System.Collections.Generic; using Newtonsoft.Json; using UnityEngine; namespace WL.Equip { [System.Serializable] public class WLGradeTier { public int tier; public string code; public string name; public string color; public double statPercent; public int isS; } // #973 code = G1~G8(표시 「전설(G5)」) [System.Serializable] public class WLGradeFrMap { public int frGrade; public int gradeLv; public int tier; public int plus; } /// 부위 출력 — output = 힘민지 eStat 이름(ATK · MaxHP · DEF) 또는 WL 특수값(#879 PD 10-16): RING = 반지 근접/원거리 공격력(무기 계열이 맞을 때만 ATK) · MOVE = 신발 이동 속도 · RANDOM = 목걸이 랜덤 스탯(둘 다 WL.PartSkill.WLPartStats 가 배율로 더한다). /// levelFactor = FR 레벨표 값에 곱하는 환산(예: 장갑 공격 칸 → 생명력 · 갑옷 체력 칸 → 방어력). [System.Serializable] public class WLGradePart { public int equipType; public string name; public string output; public string levelField; public double statFactor = 1.0; public double levelFactor = 1.0; /// #895 이 부위가 아무 효과도 주지 않는 등급들(능력치 · 파츠 스킬 0) — PD 「모든 무기의 일반 등급은 아무런 효과가 없어야」 = 무기 [1]. public int[] noEffectTiers; } [System.Serializable] public class WLGradeTableJson { public List tiers; public List frMap; public List parts; } public static class WLEquipGrades { public static bool Loaded { get; private set; } public static string LastLog { get; private set; } = ""; public static int MaxTier { get; private set; } static readonly Dictionary s_tiers = new Dictionary(); static readonly Dictionary s_frMap = new Dictionary(); // key = frGrade*1000 + gradeLv static readonly Dictionary s_tierBase = new Dictionary(); // tier → plus 0 인 FR 단계 static readonly Dictionary s_parts = new Dictionary(); static readonly Dictionary s_statCache = new Dictionary(); public static bool EnsureLoaded() { if (Loaded) return true; var cfg = WLEquipSettings.Instance; string path = cfg != null && !string.IsNullOrEmpty(cfg.gradeTablePath) ? cfg.gradeTablePath : "WL/WLGradeTable"; try { var ta = Resources.Load(path); if (ta == null) throw new System.IO.FileNotFoundException("Resources/" + path + ".json 없음"); var t = JsonConvert.DeserializeObject(ta.text); s_tiers.Clear(); s_frMap.Clear(); s_tierBase.Clear(); s_parts.Clear(); MaxTier = 0; if (t.tiers != null) for (int i = 0; i < t.tiers.Count; i++) { var g = t.tiers[i]; if (g == null) continue; s_tiers[g.tier] = g; if (g.tier > MaxTier) MaxTier = g.tier; } if (t.frMap != null) for (int i = 0; i < t.frMap.Count; i++) { var m = t.frMap[i]; if (m == null) continue; s_frMap[m.frGrade * 1000 + m.gradeLv] = m; WLGradeFrMap cur; if (!s_tierBase.TryGetValue(m.tier, out cur) || m.plus < cur.plus) s_tierBase[m.tier] = m; } if (t.parts != null) for (int i = 0; i < t.parts.Count; i++) { var p = t.parts[i]; if (p != null) s_parts[p.equipType] = p; } Loaded = s_tiers.Count > 0; LastLog = "등급 표 로드 — 등급 " + s_tiers.Count + " · FR 대응 " + s_frMap.Count + " · 부위 " + s_parts.Count; if (cfg != null && cfg.verboseLog != 0) Debug.Log("[WL-823] " + LastLog); } catch (System.Exception ex) { LastLog = "등급 표 로드 실패 — " + ex.Message; Debug.LogError("[WL-823] " + LastLog); } return Loaded; } public static WLGradeTier Tier(int tier) { EnsureLoaded(); WLGradeTier g; return s_tiers.TryGetValue(tier, out g) ? g : null; } public static WLGradePart Part(eFREquipmentType t) { EnsureLoaded(); WLGradePart p; return s_parts.TryGetValue((int)t, out p) ? p : null; } /// #895 — 그 부위가 이 등급에서 아무 효과도 없는가(parts[].noEffectTiers). public static bool NoEffect(WLGradePart part, int tier) { if (part == null || part.noEffectTiers == null) return false; for (int i = 0; i < part.noEffectTiers.Length; i++) if (part.noEffectTiers[i] == tier) return true; return false; } /// #895 — 이 장비가 지금 등급에서 아무 효과도 없는가(무기 일반 = 능력치 · 파츠 스킬 0). public static bool NoEffect(ItemJson j) { if (j == null || !EnsureLoaded()) return false; WLGradeTier t; int plus; return TryMap(j, out t, out plus) && NoEffect(Part(j.EquipmentType), t.tier); } /// WL 등급의 기본(plus 0) FR 단계 — 상자 지급 아이템을 고를 때 쓴다. public static WLGradeFrMap BaseStepOfTier(int tier) { EnsureLoaded(); WLGradeFrMap m; return s_tierBase.TryGetValue(tier, out m) ? m : null; } /// FR (ItemGrade, GradeLv) → WL 등급(+단계). 대응이 없으면 false. public static bool TryMap(ItemJson j, out WLGradeTier tier, out int plus) { tier = null; plus = 0; if (j == null || !EnsureLoaded()) return false; WLGradeFrMap m; if (!s_frMap.TryGetValue((int)j.ItemGrade * 1000 + j.GradeLv, out m)) return false; tier = Tier(m.tier); plus = m.plus; return tier != null; } public static eStat OutputStat(WLGradePart p) { if (p == null || string.IsNullOrEmpty(p.output)) return eStat.ATK; eStat s; if (s_statCache.TryGetValue(p.output, out s)) return s; if (p.output == "RING") s = eStat.ATK; // 반지 = 근접/원거리 공격력(무기 계열이 맞을 때만) else if (!System.Enum.TryParse(p.output, out s)) s = eStat.ATK; s_statCache[p.output] = s; return s; } /// 평평한 값(공격 · 생명 · 방어 · 반지)이 아닌 배율 출력(신발 MOVE · 목걸이 RANDOM) — WLEquipStats 합계에서 빠지고 WLPartStats 가 맡는다. public static bool IsRateOutput(WLGradePart p) { return p != null && (p.output == "MOVE" || p.output == "RANDOM"); } public static bool IsRingOutput(WLGradePart p) { return p != null && p.output == "RING"; } public static string OutputName(eStat s) { switch (s) { case eStat.ATK: return "공격"; case eStat.MaxHP: return "생명"; case eStat.DEF: return "방어"; } return s.ToString(); } /// 표시용 등급 라벨 — 「전설(S)+1」. rich = TMP 색 태그 포함. 대응이 없으면 FR 등급 이름. public static string Label(ItemJson j, bool rich) { WLGradeTier t; int plus; if (!TryMap(j, out t, out plus)) { if (j == null) return "?"; return WLEquipTables.GradeName(j.ItemGrade) + (j.GradeLv > 0 ? "+" + j.GradeLv : ""); } return Compose(t, plus, rich); } public static string TierLabel(int tier, bool rich) { var t = Tier(tier); return t == null ? ("등급" + tier) : Compose(t, 0, rich); } /// FR (등급, 단계) 값만으로 라벨 — 합성표 행처럼 ItemJson 이 없는 자리에 쓴다. public static string LabelOf(eFRItemGrade g, int gradeLv, bool rich) { if (EnsureLoaded()) { WLGradeFrMap m; if (s_frMap.TryGetValue((int)g * 1000 + gradeLv, out m)) { var t = Tier(m.tier); if (t != null) return Compose(t, m.plus, rich); } } return WLEquipTables.GradeName(g) + (gradeLv > 0 ? "+" + gradeLv : ""); } static string Compose(WLGradeTier t, int plus, bool rich) { string s = t.name + (!string.IsNullOrEmpty(t.code) ? "(" + t.code + ")" : t.isS != 0 ? "(S)" : "") + (plus > 0 ? "+" + plus : ""); // #973 PD 용어 「전설(G5)」 — 코드가 있으면 S 표시 대신 if (!rich || string.IsNullOrEmpty(t.color)) return s; return "" + s + ""; } /// 등급 표 전체를 한 줄로(UI 안내용). public static string Legend(bool rich) { if (!EnsureLoaded()) return LastLog; var sb = new System.Text.StringBuilder(); for (int i = 1; i <= MaxTier; i++) { var t = Tier(i); if (t == null) continue; if (sb.Length > 0) sb.Append(" "); sb.Append(Compose(t, 0, rich)).Append(' ').Append(t.statPercent.ToString("F0")).Append('%'); } return sb.ToString(); } } }