Project_WL/Assets/WL/Equip/WLEquipGrades.cs

184 lines
11 KiB
C#

// ─────────────────────────────────────────────────────────────────────────────
// 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; }
/// <summary>부위 출력 — output = 힘민지 eStat 이름(ATK · MaxHP · DEF) 또는 WL 특수값(#879 PD 10-16): RING = 반지 근접/원거리 공격력(무기 계열이 맞을 때만 ATK) · MOVE = 신발 이동 속도 · RANDOM = 목걸이 랜덤 스탯(둘 다 WL.PartSkill.WLPartStats 가 배율로 더한다).
/// levelFactor = FR 레벨표 값에 곱하는 환산(예: 장갑 공격 칸 → 생명력 · 갑옷 체력 칸 → 방어력).</summary>
[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;
/// <summary>#895 이 부위가 아무 효과도 주지 않는 등급들(능력치 · 파츠 스킬 0) — PD 「모든 무기의 일반 등급은 아무런 효과가 없어야」 = 무기 [1].</summary>
public int[] noEffectTiers; }
[System.Serializable] public class WLGradeTableJson { public List<WLGradeTier> tiers; public List<WLGradeFrMap> frMap; public List<WLGradePart> 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<int, WLGradeTier> s_tiers = new Dictionary<int, WLGradeTier>();
static readonly Dictionary<int, WLGradeFrMap> s_frMap = new Dictionary<int, WLGradeFrMap>(); // key = frGrade*1000 + gradeLv
static readonly Dictionary<int, WLGradeFrMap> s_tierBase = new Dictionary<int, WLGradeFrMap>(); // tier → plus 0 인 FR 단계
static readonly Dictionary<int, WLGradePart> s_parts = new Dictionary<int, WLGradePart>();
static readonly Dictionary<string, eStat> s_statCache = new Dictionary<string, eStat>();
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<TextAsset>(path);
if (ta == null) throw new System.IO.FileNotFoundException("Resources/" + path + ".json 없음");
var t = JsonConvert.DeserializeObject<WLGradeTableJson>(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; }
/// <summary>#895 — 그 부위가 이 등급에서 아무 효과도 없는가(parts[].noEffectTiers).</summary>
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;
}
/// <summary>#895 — 이 장비가 지금 등급에서 아무 효과도 없는가(무기 일반 = 능력치 · 파츠 스킬 0).</summary>
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);
}
/// <summary>WL 등급의 기본(plus 0) FR 단계 — 상자 지급 아이템을 고를 때 쓴다.</summary>
public static WLGradeFrMap BaseStepOfTier(int tier) { EnsureLoaded(); WLGradeFrMap m; return s_tierBase.TryGetValue(tier, out m) ? m : null; }
/// <summary>FR (ItemGrade, GradeLv) → WL 등급(+단계). 대응이 없으면 false.</summary>
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<eStat>(p.output, out s)) s = eStat.ATK;
s_statCache[p.output] = s;
return s;
}
/// <summary>평평한 값(공격 · 생명 · 방어 · 반지)이 아닌 배율 출력(신발 MOVE · 목걸이 RANDOM) — WLEquipStats 합계에서 빠지고 WLPartStats 가 맡는다.</summary>
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();
}
/// <summary>표시용 등급 라벨 — 「전설(S)+1」. rich = TMP 색 태그 포함. 대응이 없으면 FR 등급 이름.</summary>
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);
}
/// <summary>FR (등급, 단계) 값만으로 라벨 — 합성표 행처럼 ItemJson 이 없는 자리에 쓴다.</summary>
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 "<color=" + t.color + ">" + s + "</color>";
}
/// <summary>등급 표 전체를 한 줄로(UI 안내용).</summary>
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();
}
}
}