2026-10-18 15:09:05 +00:00
// ─────────────────────────────────────────────────────────────────────────────
2026-10-22 15:24:35 +00:00
// WLEquipGrades.cs — WL 아이템 등급 표 로더 (PD 2026-10-09 · #823 7단계 → 2026-10-23 #973 8단계 G1~G8 · 천상 = FR Legend-3)
2026-10-18 15:09:05 +00:00
/ /
// 표 = `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
{
2026-10-22 15:24:35 +00:00
[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)」)
2026-10-18 15:09:05 +00:00
[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 )
{
2026-10-22 15:24:35 +00:00
string s = t . name + ( ! string . IsNullOrEmpty ( t . code ) ? "(" + t . code + ")" : t . isS ! = 0 ? "(S)" : "" ) + ( plus > 0 ? "+" + plus : "" ) ; // #973 PD 용어 「전설(G5)」 — 코드가 있으면 S 표시 대신
2026-10-18 15:09:05 +00:00
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 ( ) ;
}
}
}